Neural Network Game Physics Engine
Bouncing-ball simulator in C++ whose collisions come from a neural net trained on analytic physics.
4 min read
A small neural net learns 2D elastic circle-circle collision physics from analytic ground truth, and a hand-written C++ forward pass uses that trained net to drive a real-time bouncing-ball simulation, in place of recomputing the analytic collision formula at each collision event.
What it does
python/train.pygenerates ~12,000 synthetic collision samples using the real analytic 2D elastic-collision equations (conservation of momentum/energy along the line of centers), trains ansklearn.neural_network.MLPRegressor(12 inputs -> 4 outputs, two 128-unit ReLU hidden layers) to predict post-collision velocities from pre-collision state, reports held-out test error, and exports the trained weights, biases, architecture, and input/output scaling todata/weights.json.cpp/sim.cpploadsdata/weights.jsononce at startup, and on every ball-ball collision event runs a hand-written ~30-line forward pass (matrix-vector multiply + ReLU per layer, no NN library) to get post-collision velocities. It also implements the analytic physics directly in C++, runs both a NN-driven world and an analytic-driven world side by side from identical initial conditions, and reports the numeric divergence between them over the run. Wall bounces use simple analytic reflection directly (trivial, not worth a learned model). Rendering is an ASCII grid printed to the terminal each frame.
Build & run (exact order)
# 1. Train the network and export weights.json (uses the shared project venv)
cd "05-nn-physics-engine/python"
../../.venv10projects/bin/python train.py
# -> prints held-out test MSE/RMSE and a few predicted-vs-analytic examples
# -> writes ../data/weights.json
# 2. Build the C++ simulator (uses nlohmann-json, installed via `brew install nlohmann-json`)
cd ../cpp
clang++ -std=c++17 -O2 -I/opt/homebrew/Cellar/nlohmann-json/3.12.0/include -o sim sim.cpp
# 3. Run it
./sim # prints divergence stats every 20 steps + summary
./sim --render # also prints ASCII frames every 40 steps
Architecture
python/train.py analytic physics (numpy) -> dataset -> MLPRegressor
|
v
data/weights.json {layer_sizes, activation, weights, biases,
input_mean/scale, output_mean/scale}
|
v
cpp/sim.cpp NeuralNet::load() -> NeuralNet::forward() (hand-written forward pass)
analytic_collision() (hand-written ground truth)
step() drives two parallel Ball worlds each frame, ASCII render
weights.json is the contract between the two sides: sklearn’s .coefs_[i] is a
(n_in, n_out) matrix and .intercepts_[i] is a (n_out,) vector per layer. The C++
NeuralNet::forward() indexes them the same way sklearn produces them, so no
transposition or reshaping is needed on load.
Results from a real run
Held-out test set (2,400 samples, 20% split), MLPRegressor(128, 128), ReLU (the physics self-check for momentum and kinetic energy conservation passed first):
- Test MSE: 0.0305 (post-collision velocity, 4 components)
- Test RMSE: 0.175 (velocities in this dataset range roughly -8..8)
Sample predicted vs. analytic ground truth (test set):
analytic=[ 1.9641 -1.4199 0.6505 2.6295] predicted=[ 1.8881 -1.3697 1.1114 2.7731]
analytic=[ 7.3865 -2.0447 3.87 2.0057] predicted=[ 7.2223 -2.0562 4.0924 2.0556]
C++ simulation, 5 balls, 200 steps, NN-driven world vs. analytic-driven world from identical initial conditions:
step 0 mean position divergence: 0.0
step 20 mean position divergence: 0.171
step 60 mean position divergence: 0.558
step 100 mean position divergence: 4.876
step 180 mean position divergence: 8.830
RMSE of NN-vs-analytic position divergence over the run: 8.49
Max single-ball position divergence observed: 26.19
Early frames track closely (sub-0.5 units of divergence for the first ~15 collisions). Divergence grows superlinearly afterward. This is expected: bouncing balls in a box are a chaotic dynamical system, so the ~5-10% per-collision velocity error from the NN compounds: a small perturbation at collision N changes the trajectory into collision N+1, which changes the next collision geometry, etc. This is a real, honest limitation of using a learned approximator for a chaotic simulation loop, not a bug.
Simplified vs. full spec
- 2D circle-circle collisions only, no walls-as-NN-input, no cloth/fluid/soft-body. Add rigid-body rotation/torque, polygon collision, or soft-body/cloth constraints when the portfolio piece needs more than bouncing circles.
- sklearn
MLPRegressor, not a physics-informed neural network (PINN). Add an energy-conservation penalty term to the loss (e.g. a custom PyTorch training loop) when prediction accuracy over long rollouts matters more than a quick MVP. - ASCII-grid terminal rendering, not a real game engine/GUI. Swap in SFML/raylib (or a browser canvas) for real graphics when this needs to look like a game instead of prove the physics loop.
- Divergence measured only as summary statistics over one run/seed, no systematic accuracy benchmark across many initial conditions. Add a batch harness that runs N random seeds and reports divergence distribution when accuracy claims need to be more rigorous.
Support
Submit issues, questions, or bug reports to the GitHub issue tracker: https://github.com/ADM1SH/nn-physics-engine/issues
Roadmap
- Done: Core architecture and baseline implementation.
- Done: Functional verification and test coverage.
- To do: Train on non-elastic collisions and friction coefficients
- To do: Add real-time OpenGL particle visualization
Contributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/improvement. - Commit your changes:
git commit -m "feat: enhance functionality". - Push to the branch:
git push origin feature/improvement. - Open a Pull Request.
Authors and Acknowledgment
- Adam Anwar (ADM1SH) - Lead architect and developer.
- Architected by Adam Anwar. Explores surrogate neural network approximation of rigid body mechanics.
License
Licensed under the MIT License. See LICENSE for details.
Project Status
Functional simulation sandbox.