WIP: Simple networking deterministic 2D physics engine
C#, Unity, Photon(PUN)
In 2020, I started developing my own engine that utilizes Unity for rendering and handling input. I used Photon Unity for creating rooms and data transmission, though it wasn't suitable for synchronizing objects. The engine is decoupled from Unity but includes several data representation methods and serialization. Here's why I decided to use my own physics engine instead of Unity's:
- I wanted a multiplayer experience without relying on a master server. Photon Unity worked well for player-to-player data transmission, but it handled objects within the scene in a limited way. Creating an object didn't allow for passing initial data in a type-safe manner, and any post-creation data assignment needed synchronization, causing performance overhead, even if not all data changed. While Unity's RPC methods are available, they primarily support primitive types. Creating custom types is limited and becomes cumbersome for each object. The object pooling system also had its complications, as each object needed a unique identification ID for efficient gameplay logic, which could lead to issues when objects were destroyed on the Photon side.
- Photon's synchronized fields required changes to be made only by the owner of the object, which led to a significant amount of code to ensure these restrictions. This seemed impractical because it felt that each player could calculate these changes if the game logic was deterministic.
- Unity physics is non-deterministic due to the use of floating-point calculations, which means that the same game logic can't be simulated identically across different devices. To mitigate this, object synchronization across all players and a significant amount of data transmission is needed. In a deterministic engine, FixedPoint data types replace floats to maintain precision, transmitting only player inputs.
- Unity's physics is also restrictive due to callback methods. Identifying which object touched another becomes complicated, as it's necessary to identify the object on the Photon side based on the Unity component, and then cross-check it with our created ID to ensure accuracy. This becomes even more challenging when verifying that these objects belong to the respective player.
- The engine needed to accommodate mid-game player joins, which added a layer of complexity to the codebase. Problems arose when not all objects were in the scene. Implementing game saves was equally challenging.
How did my engine solve these issues?
- It features simple 2D physics with trigger, dynamic, and static colliders, each having type-safe IDs for easy reference. Different collider types are available, including rectangular and circular shapes. This suits well for creating top-down games. However, it may not be suitable for side-view games with rotated objects.
- The engine generates code by starting with pseudo-code in a special file, which is used to create C# classes. Each reference class has an ID class. Serialization, deserialization, toString functions, and initialization methods are provided for each class. The project is divided into two C# projects: Unity and simulation, ensuring separation from Unity's code and non-deterministic logic.
- Rollbacks save all changes in values, allowing for the restoration of older data if needed. This helps re-simulate the required values up to the current state.
- Object pooling is straightforward on the Unity side, efficiently creating, destroying, or disabling objects when necessary.
- Each device executes the same code, ensuring an identical game experience. A check is in place to detect discrepancies caused by players attempting to manipulate values, which promptly flags such actions, allowing for the exclusion of the offending player from the match.
- With the transmission of only player inputs, the data stream is minimal, allowing the game to be played even with a weaker internet connection.
- RPC commands are also available, working seamlessly with rollbacks and supporting various serializable types.
Remaining tasks:
- Continuous refinement of the code generation process.
- Ensuring identical code execution across devices.
- Testing how the game handles one player experiencing slowdown while others continue.
- Resolving casting issues in the physics system.
- Please note that while many challenges have been addressed, there's still work to be done to further develop this engine.

Comments
Post a Comment