derkork / godot-statecharts

A state charts extension for Godot 4
MIT License
734 stars 35 forks source link

Can I force the state machine into some state? For network syncs #53

Closed amireldor closed 9 months ago

amireldor commented 10 months ago

Hi. I'm using Godot 4 high-level networking and I got a scene that uses State Charts. When a client connects to a running game session, I want the state of that scene to "jump" or be "forced into" the state that's currently on the server. Is that possible with the current API?

Thanks.

derkork commented 10 months ago

I have really no experience with multiplayer, so I'm afraid I can't offer much advice on that front. If you want to replicate the state of the state machine there are basically three things you would need to synchronize:

At least for the first and second part there is actually code built-in to save/restore this, though it's not part of the public API. It is internally used for the history states.

Every state has a _state_save method which stores the state and all substates including any pending transitions into a SavedState object. This can be restored by calling _state_restore. So theoretically you should be able to do something along the lines of:

var saved_state = SavedState.new()
$StateChart._state._state_save(saved_state)

to save the state of the state chart into a SavedState object which you would then need to replicate to the client. On the client you'd need to call:

$StateChart._state._state_restore(replicated_state_from_server)

You'd still need to manually save/restore the expression properties which are saved in the _expression_properties internal dictionary oft the state chart.

So theoretically it should be possible but not with the public API and because multiplayer is what it is there will probably be some extra issues popping up that I cannot really foresee.

But this is interesting in any case since it's also a problem that would come up when trying to save/load the game. So maybe it would be good if there was some public API which would allow you to save the whole state of the state chart into some Resource object and restore it from there.

amireldor commented 10 months ago

Hi @derkork and thank you for the information.

I have looked at SavedState and your instructions might be helpful. I'll take a look at _expression_properties as well.

Being useful for a use-case saving/loading games, do you have any directions on how can I approach this and try to provide a PR? Any API you were thinking of?

Additionally, I tried a different approach as well and you might be able to help with that. I try to create a Transition "on-the-fly" and have the state chart run that. I tried adding this code to the base state.gd. What it does, in my server-authoritive game, when any state is ready on a client, it requests a sync from the server (rpc to id 1 in Godot high-level networking). The server tells the requesting client the active state of the node, and back on the client side I try to create a transition if the state node on the server was active.

func _ready():
    if not multiplayer.is_server():
        request_network_sync.rpc_id(1)

@rpc("any_peer")
func request_network_sync():
    if not multiplayer.is_server():
        return
    set_network_sync.rpc_id(multiplayer.get_remote_sender_id(), _state_active)

@rpc("authority")
func set_network_sync(active_sync: bool):
    if active_sync:
        # THIS IS NOT WORKING PROPERLY
        var transition = Transition.new()
        transition.to = get_path()
        _run_transition(transition)

With this I get errors like the following and it's too late now to try and solve them :) but maybe you'd have an idea on how to achieve this quasi-state-transition better:

E 0:00:01:0235   transition.gd:60 @ resolve_target(): Can't use get_node() with absolute paths from outside the active scene tree.
  <C++ Error>    Condition "!data.inside_tree && p_path.is_absolute()" is true. Returning: nullptr
  <C++ Source>   scene/main/node.cpp:1544 @ get_node_or_null()
  <Stack Trace>  transition.gd:60 @ resolve_target()
                 atomic_state.gd:9 @ _handle_transition()
                 state_chart.gd:116 @ _run_transition()
                 state.gd:65 @ _run_transition()
                 state.gd:310 @ set_network_sync()
derkork commented 9 months ago

The API I had in mind was some variation of the one that is already there but on the state chart level. E.g. ideally something along the lines of:

var saved_state:SavedStateChart = state_chart.save_state()

SavedStateChart would be some resource type that you can either save directly or send over the wire.

To restore, you would do something like:

var saved_state:SavedStateChart = ... # load from file or receive over the network
state_chart.restore_from(saved_state)

The tricky part will be to save all the moving parts and make sure nothing is left in some inconsistent state (e.g. queued events still running, etc.).


As for your problem with the transition. Transitions need to be in the tree in order to run. This is because they call get_path to resolve the target state. So you would need to add them below the state for which they should run. I haven't tested what happens when you modify state charts at runtime and I'm pretty sure this will currently lead to interesting (read unwanted) behaviour. They will probably be ignored by normal events as the transitions are cached internally during initialization. If it's just a one-off it may work but it feels like a really brittle solution.

amireldor commented 9 months ago

Thanks for the answers. However, I'm afraid I'll be choosing a different state machine solution for my current project. Your library is awesome but it might be an overkill for me at the moment, and the network/serialization thing is essential.

derkork commented 9 months ago

Yeah makes total sense. Right now not being able to synchronize the state is a total showstopper for your case. Good luck with your game!