foxssake / netfox

Addons for building multiplayer games with Godot
https://foxssake.github.io/netfox/
MIT License
404 stars 16 forks source link

Add rollback to Projectiles #312

Open TheYellowArchitect opened 1 month ago

TheYellowArchitect commented 1 month ago

:sparkles: Description

Projectiles don't work with Rollback:

"Upon firing, sending a request to the server and waiting for the response with the projectile would introduce a delay. Doing a full-on state synchronization with MultiplayerSynchronizer or RollbackSynchronizer can be unfeasible with too many projectiles, and unnecessary, since most of the time, projectiles act and move the same way regardless of their surroundings."

Rollback currently works per entity, using RollbackSynchronizer. I agree in the above that each Projectile should not have a RollbackSynchronizer but what should exist to make them viable for rollback is critical events to track which tick it was created, which tick it was destroyed (this logic can be used for more entities btw)

So, at the NetworkRollback level, there would be a ProjectileHistorian node, which stores a Dictionary of Projectiles, and important data for them.

Code Use-Case

A projectile is spawned at tick 57, and explodes at tick 78. The game is at tick 79, and a rewind happens to tick 75. I assume this would happen if a new input is received for tick 75.

On rewinding to any tick, ProjectileHistorian checks if a projectile which no longer exists on this tick (79) exists on the target tick (75) and if so, it instantiates its PackedScene.

Data

Taking the above code use-case in mind, here is what a ProjectileHistorian must store for every projectile:

  1. Its ID
  2. Its PackedScene (for instantiating it on rollback)
  3. Its position, rotation, velocity for every tick.
  4. Creation tick
  5. Death tick
class_name ProjectileData

var id: int
var projectile_instance: Node
var creation_tick: int
var death_tick: int
var scene_template: PackedScene
class_name ProjectileProperties

var position: Vector3
var rotation: Quaternion
var velocity: Vector3

So ProjectileHistorian simply has a Dictionary<ProjectileID, ProjectileData> and a Dictionary<ProjectileID, Dictionary<tick, ProjectileProperties>>

We could even extrapolate the creation ticks and death ticks completely, and hence probably skip/simplify ProjectileData. The first tick in Dictionary<ProjectileID, Dictionary<tick, ProjectileProperties>> is the creation tick. The death tick becomes a function, e.g.


#pseudocode
func get_death_tick() -> int:
    #basically check if it is not freed
    if (projectile.is_in_the_scene):
        return -1
    else:
        return ticks_dictionary.keys().max()

In this way, ProjectileData is needless. You need 2 primitive dictionaries instead in its place:

var projectiles_stored: Dictionary #<id, Projectile>
var projectiles_scenes: Dictionary #<id, PackedScene>

And to register the projectile properties on the ticks:

var projectiles_status: Dictionary #<id, Dictionary<tick, ProjectileProperties>>

The above 3 dictionaries is the only dictionaries the ProjectileHistorian needs.

Btw, it is certain that this ProjectileHistorian must hook into some signals from NetworkRollback.

Use-Case

I cannot think of any online game, 2D or 3D not using projectiles of some sort. Metroidvanias, Strategy, FPS, Action Games etc And given NetworkWeapon is included in netfox.extras, this ensures projectiles work with the rest of netfox's rollback.

Distribution

Extension of NetworkWeapon logic, so netfox.extras

Notes

Requires the resolution of https://github.com/foxssake/netfox/issues/253 before starting a PR on this.

Once implemented, should be used by the forest brawl example.

elementbound commented 3 weeks ago

On rewinding to any tick, ProjectileHistorian checks if a projectile which no longer exists on this tick (79) exists on the target tick (75) and if so, it instantiates its PackedScene.

I'm not convinced that destroying and re-instantiating nodes is the best way to go about managing liveness with rollback. Also pulling all projectiles into a centralized object like this goes against the current design philosophy of letting things to individually manage themselves by reacting to central signals. Either way, I'm also not convinced that projectiles should be pulled into a centralized ProjectileHistorian.


On data, this is also very game specific. I'm also not convinced that each projectile needs to have position stored, e.g. if the projectile's movement logic is very simple. Think of RTS games where projectiles get spawned at some location, and then continuously march toward a target unit. You don't need to store location for every tick, you can just calculate the position based on lifetime, starting position and the target unit's position.

There's also a reason why e.g. RollbackSynchronizer has configurable state properties - when developing netfox, I don't know the game you will be building. And I try to make the least amount of assumptions. Deciding that every projectile must have a position, rotation and velocity breaks that in a big way.


Extension of NetworkWeapon logic, so netfox.extras

NetworkWeapon does not care about its projectiles much, so I don't see how this would be an extension of it. But projectile logic in general fits in extras, that's true.


So to clarify, you're requesting an example on how to do projectiles with rollback, right?

TheYellowArchitect commented 2 weeks ago

I'm not convinced that destroying and re-instantiating nodes is the best way to go about managing liveness with rollback.

Pooling can be utilized, which stores like the last X projectiles of each PackedScene :+1:

Also pulling all projectiles into a centralized object like this goes against the current design philosophy of letting things to individually manage themselves by reacting to central signals.

I agree, but how would you implement this feature, when Projectiles themselves are freed? The only answer is to hide+disable the projectile on impact, and free it when rollback history limit is reached. But when a rollback happens after it is destroyed, it must hook to some central signals somewhere, to know to reactivate+reposition itself. NetworkRollback itself shouldn't be related to Projectiles, so ProjectileHistorian can be used. Though I guess, the logic could move from the ProjectileHistorian to the Projectiles themselves, via some static function.

I'm also not convinced that each projectile needs to have position stored, e.g. if the projectile's movement logic is very simple. Think of RTS games where projectiles get spawned at some location, and then continuously march toward a target unit. You don't need to store location for every tick, you can just calculate the position based on lifetime, starting position and the target unit's position.

Didn't think of that, sounds like it saves a lot of memory :+1:

There's also a reason why e.g. RollbackSynchronizer has configurable state properties - when developing netfox, I don't know the game you will be building. And I try to make the least amount of assumptions. Deciding that every projectile must have a position, rotation and velocity breaks that in a big way.

I agree. Making usage of PropertyEntry sounds good here, since that class already exists

So to clarify, you're requesting an example on how to do projectiles with rollback, right?

yup, I need this for my game (ideally with projectiles included in inputs so I can have a clean replay system)

TheYellowArchitect commented 1 week ago

I'm also not convinced that each projectile needs to have position stored, e.g. if the projectile's movement logic is very simple. Think of RTS games where projectiles get spawned at some location, and then continuously march toward a target unit. You don't need to store location for every tick, you can just calculate the position based on lifetime, starting position and the target unit's position.

There's also a reason why e.g. RollbackSynchronizer has configurable state properties - when developing netfox, I don't know the game you will be building. And I try to make the least amount of assumptions. Deciding that every projectile must have a position, rotation and velocity breaks that in a big way.

I agree. In my case, I want a grenade, so I do need its position since it bounces off walls/players. So having the position as a state property (not hardcoded) ftw