Hey team,
We’re having some difficulty with our UI lifetime vs gameplay lifetimes
A common problem we’re running into, is that the HUD can get constructed before the PlayerState exists on clients
As an example, we’re starting to use ViewModel Resolvers to get ViewModels at widget creation time without the boilerplate of having to pass ViewModels down the UI tree, or have boilerplate in our widgets to fetch things, (As an aside: ViewModel Resolvers are in my opinion, a fantastic solution to supporting UI iteration time by allowing the widgets to pull context, without losing other great benefits you get from SoC the MVVM like mockability by forcing ‘pulling’ behaviors into the ViewModel itself).
But the limitation we’re hitting with them, is that in multiplayer, we sometimes encounter cases where the HUD gets constructed before the PlayerState gets replicated, so when our Resolver gets the owning player, and tries to get a ViewModel from the owning players PlayerState, it might fail because the PlayerState isn’t guaranteed to exist yet.
We have the Widget Extensions system from Lyra, which is super helpful for avoiding these timing issues since you can push extensions with a context object, but it isn’t always a viable solution.
As another example here, we have some UI which displays state for the players team, to do that we get the team information from the GameState, and find the widgets owning players team, but if the owning player doesn’t have a PlayerState yet, then we can’t find their team.
Is there any high level architectural guidance around UI and gameplay that we’re missing which could potentially help us avoid these race conditions?
[Attachment Removed]
Hi,
I’ve certainly spoken with others who have run into similar issues, it’s a bit of a challenge to come up with a one-size-fits-all approach. Internally, we lean towards resolvers heavily where we can but use manual assignment for cases where we need to wait on data. Of course, then you’re dealing with boilerplate plumbing logic again.
I’ve experimented with a deferred approach where I create a resolver that fetches a VM from some subsystem (or anywhere, really) and that subsystem will return a blank dummy VM and sign the widget up for a deferred update if the data isn’t available. Then, once whatever I need is available, I can iterate through my list of subscribed widgets and push their “actual” VM to them. A visibility flag on the VM can make it easy to collapse the widget until the data is available.
This approach has worked pretty well in some smaller scale tests I’ve done, though I could see it getting complex to track who is waiting for what data depending on your project. If it’s a fairly simple case (like waiting for player 3’s PlayerState) then I’d give that a try and see if it works for you, as you still get the advantages of obfuscating away the “where does my VM come from” details for whoever is designing the widget itself. Note that you’ll need to enable Create Public Setter on the widget, so that you can push the new VM to it when ready.
The other approach you could take here would be to be really diligent about tying widget lifetime to the underlying data the widget is meant to reflect, though it sounds like you’ve probably explored that with the Widget Extensions in Lyra. Subsystems in general give you good “lifetime” hooks for different systems, so initializing bits of UI in a subsystem can guarantee the data exists and give you a chance to clean the widget up once the data no longer exists.
Best,
Cody
[Attachment Removed]
Thanks for the insight Cody, I was quietly hopeful that we’d just done something silly and there would be an easy solution
A pattern that we use with Subsystems that’s similar to what you’ve outlined, is that we have our Subsystems return a blank dummy VM, which we also store a reference to in the Subsystem then when the data gets populated by the Subsystem, we update that ViewModel instance, which updates the data on our widget rather than binding to an event for when the data becomes available
Using widget extensions is still a valid option for this use case, we’ll keep noodling on it internally
Thanks for the insight!
[Attachment Removed]