A real-time game from scratch - Physics, TDD and Core Game Loops [4]
November 27, 2020
Previous Entries
Introduction
Moving a spinning yellow box is not particularly interesting or engaging. I am trying to write a space game after all.
To continue to head in that direction, I will begin implementing some basic physics. The challenge here is two-fold:
- Realistic physics as if the player were in space
- Having the physics calculated on the server side and propogated to the client
Space Physics
Physics in space is a bit weird if you have never been exposed to the math behind it. Since there is no friction to speak of in the vacuum of space, Newton’s laws of motion display themselves beautifully when compared to how they are exhibited under Earth conditions.
The laws are as follows:
- If a body is at rest it will remain at rest. If a body is moving, it will keep moving at constant speed in a straight line. These hold unless the object is acted upon by a force.
- The acceleration of a body is proportional to the force applied to it and its mass. Conventionally known as F = m * a.
- For every action, there is an equal and opposite reaction.
Newton’s First Law… in space
Imagine a ball sitting in space. It is not moving (it actually is if you look at it on a solar system or galactic scale, but let us keep things simple). Given nothing ever coming into contact with it, it will remain as it is: motionless.
Now imagine an alien passes by and kicks the ball towards our sun. The ball now travels freely and smoothly in a straight line. With no one or nothing else interacting with it, it will happily keep moving towards the sun at the same speed (I am leaving out gravitational pulls for simplicity). There is no friction to take away from the kinetic energy that has been given to the ball.
Newton’s First Law… in code
Currently in our case, the inputs from the keyboard which affect the spinning square are the equivalent of the alien kick mentioned above - we are applying a force to a motionless object which is sitting in a no-friction vaccuum (AKA outer space).
If we leave the ideas of force, mass and acceleration out of the picture for now (we will cover that later when we get to Newton’s Second Law), then the expectation of the movement of our spinning square is very simple: If we move it in a direction, we expect it to keep moving in that direction, until we move it in a different direction.
To write the code implementing the above statement, we can use a software development methodology called Test Driven Development. The idea is that we will write a program which will verify the behaviour of our physics program for us. If the tests pass, then the logic is correct, otherwise we should investigate as we may have introduced a bug, or our expectation of the behaviour of the physics program is incorrect.
For our physics (which only considers the first law), I wrote a function which takes a position, a direction (which may be nil
, meaning no direction was passed. nil
).
Accompanying this, I wrote a function which takes a position, the expected position and a direction. This second function simply calls the first one and compares the result with the expected result. If there is a difference, then an error is thrown, otherwise the test passes.
This form of automated testing helps ensure that the logic of my code is correct and remains correct throughout the development of the project. If the test begins failing, then either the code was changed in a way that violates the expected logic or the logic has changed and now the code needs to be changed to match it.
Core Game Loops
To apply the code written for our initial physics engine (which for now covers Newton’s First Law only), we need to reimagine the paradigm of how our server will work.
In its current form, the game state is only ever updated when the client sends data to the server. Now that we are introducing phyics, the game state update should be calculated regardless of whether the client send data or not. The data the client sends should count for the next physics update being calculated.
The above means that we need to implement a core game loop. Different from a core gameplay loop, this is a continuous process which will receive inputs within a specific timeframe, then update the game state and push it out to all clients, even if no inputs are received.
This raises the question: how often should we update? This update time, also called a tick rate determines how responsive a game will be to inputs but also how much data is passed back and forth. The higher the tick rate, the more data will be received, regardless of whether inputs are sent to the server or not.
A useful part of software is that these kind of decisions can be configurable. I do not need to take the decision now and deal with the consequences, but rather I can make the tick rate a parameter and then test out different values to try and find the sweet spot in the ratio of responsiveness to data flow.
Here begins the usage of the concurrency primitives that Go provides. The core game loop will run in its own go routine and on a timer (if our tick rate is 30 ticks per second, then we it would be 1000ms / 30 = every 33.3ms) it will send out a game update. Until that time elapses, the game will accept inputs that the server receives and reccalculate the game state.
Demo
You can find the first video demo of the above logic implemented here:
This would be trivial to implement in a client-side only fashion, however in the above demo all the game state is being calculated server-side, pushed out to the client at the end of every tick.
Conclusion
Whilst the server-side code looks like a bowl of delicious spaghetti and someone’s first concurrent project ever, it works! Whilst it is indeed a small step, it is beginning to feel like proper progress.
Given how long this article is, I have chosen to implement the rest of Newton’s Laws in another article.
The next article however, will aim at improving my DevOps skills by implementing Continuous Integration and Continuous Deployment (CI/CD) for the latest version of Cabin Fever on every push to the main
branch. This will include:
- Building the Frontend
- Building the Backend
- Ensuring that tests pass
- Deploying to a server
- Switching over from the old ones to the new
You can find the code for this article at the article-004 tag.
Next Entry