2
\$\begingroup\$

My first question here! :)

Im trying to conceptualize how I would go about recording the moves of my players, and also the state of my game after the calculations of that move.

For example, Final Fantasy tactics.

  1. UnitA makes an attack
  2. Calculations will be done
  3. Store the last move, and also the state of the game after that move.

In the example above, for example, I use Firebase firestore to update the players of the state of the game. In that firestore document, i will update it with the latest move and its effects, together with the latest state of game.

My concern is in number 3, where should I put the record of the previous moves and state of the game after those moves. The purpose of this is for example, I want to replay the match. With records of each moves and states of the game after those moves, I will be able to replay the match accurately. My concern is, it seems recording all these seems huge data because, if a unit moves, I would have to store the entire state of the game, which includes all units hp/position in tile map/buffs/ abilities states(cooldown) etc.

What I have thought of so far Option: A: (Bad)Store them in same document as the latest document snapshot. Like create an array where it will contain the record of move and snapshot for that move B: (I think better than A but so many documents will be created) Create one firestore document for each move with the state of the game after that move and store them on a separate firestore collection.

May I ask for advice.

Thanks in advance

EDIT:

The game is a typical turn based game with 2 players (for now, in my plan, i want it to be dynamic, could be 2 teams with multiple players in each team. Or 2 or more teams). But for now, just 2 players playing against each other. Each player controls units. Each unit has several abilities to use.

Abilities may Stun, apply Buff, and/or Damage etc the target unit/units. And those status may linger on on the targeted unit. For example, Silence or Stun for 2 turns. These are just few example of attribute effects, but there are more and could be further increased in the future.

Unit A, and Unit B are allies. Unit X is their enemy. On previous turn, Unit X casted a channeling stun to Unit B. Rendering Unit B unable to move. Now on new turn, Unit A stuns Unit X, freeing Unit B. This is the example i listed above. Another example is below

Unit A, and Unit B are allies. Unit X is their enemy. On previous turn, Unit X casted silence to Unit B. Now on new turn, Unit A casts a repel buff to Unit B so he can casts spells again.

Reasons why I am considering storing every state per move:

  1. Replay purposes and displaying the state of the game accurately. Say for example, replay can be paused, and the viewer of the replay can check the state of the game (units, buffs, cooldowns, etc)
  2. Debugging purposes, say for example, a bug was reported saying a certain skill isn't working as expected during a certain condition.
\$\endgroup\$
14
  • 2
    \$\begingroup\$ If you have each moves, you only need to store the first state, right, since you can get every state based on the moves your players did, can't you? \$\endgroup\$
    – Vaillancourt
    Jul 21 at 15:12
  • \$\begingroup\$ Thanks for quick reply. Thats true, I have thought about that as well. I forgot to mention above. Maybe this is what I will go for. I didn't select this before because, I felt it is quite risky. Meaning, the state of the game would be based on a read only move. Issue is, move calculations are done on cloud functions / backend. Yeah this might work, I just have to be detailed and careful on the latest move object, I guess. Thanks again. \$\endgroup\$ Jul 21 at 15:23
  • \$\begingroup\$ Is your game state the result of deterministic calculations? \$\endgroup\$
    – Pikalek
    Jul 21 at 15:43
  • 1
    \$\begingroup\$ oh my, first time encountering such term. had to google, my understanding might be wrong. I thought about this as well, the game has some random elements on it. But if such is the case, then I can just put the result of the random function in the detail of the latest move. So that on replay, it will just read the result instead of generating a new random result. I based my understanding of the deterministic calculation here en.wikipedia.org/wiki/Deterministic_system . Let me know if im wrong, Thanks \$\endgroup\$ Jul 21 at 16:08
  • \$\begingroup\$ @Vaillancourt @Pikalek the thing is, I kinda prefer saving the state of game after each move. Because if I will just store the detailed latest move, and calculate the game state from that, I feel it might be a wrong approach or bug prone. For example, a unit executes a move to remove a buff from a target. Then I must store that target's entire state in the affected units property of the latest move object. If the move targets all units in the game, then I would have to store all units as well. I just feel like reading the state instead of recalculating is simpler, though data consuming \$\endgroup\$ Jul 21 at 16:18
1
\$\begingroup\$

It depends - first of all start with whatever you have and later upgrade if problems occur. If storing whole state becomes a problem then...

...Command pattern is the way to go, as others said. Storing a state after each move is... not a way to go. No, let me rephrase. It's wasteful but good at prototyping stage. "You feel that it's bug prone". Yes, but you will squash those bugs, like all other titles do... I've never seen anyone storing EVERY state because they feel insecure.

I'm getting a vibe that you are a bit of a novice and try to overengineer something simple. Realistically speaking as long as you can replay things deterministically (storing RNG seed etc) you have a very lightweight way of keeping state. I recommend reading this free webbook about Game PRogramming Patterns to get more understanding

You can solve it all easily by combining the command pattern and storing SNAPSHOTS of state. Let's say you record a full state after 20 moves - so you have those key frames that store full state and delta moves that transform a state bit by bit. To replay you just go to snapshot and replay several actions from there. Same goes for sending state for debugging (idea taken from GDC talk about Rewind in Braid as linked in the @Charlys answer). And the best part about snapshots - they are simple - you basically store state and several actions - it's a minuscule difference from storing every state.

You can control frequency of snapshots however you want - If rewinding a replay takes too long you just increase granulity of snapshots. Determine how frequent snapshots should be done by examining what amount of moves takes an instant to replay and doesn't produce hangups.

Also to your Firebase database - usually things like that are done very infrequently, like once or twice a day when the network congestion is very low. Basically your whole state should be kept on server until it's synced with the DB. I don't exactly know how Firebase Firestore's pricing model looks like, but considering snapshots looks like a good and easy way of optimizing resource utilization.

Snapshots also allow you to test your determinism - you can detect if you have some bugs when you replay all moves from the initial state and compare it to the stored snapshots. If they diverge - you just binary search for snapshot where undeterminism was introduced.

@Edit, Because I didn't put emphasis on that - first do something that works and later if you have problems - upgrade.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thank you very much. It is true, i consider myself a novice in game development. Thanks for sharing I've never seen anyone storing EVERY state because they feel insecure. Yeah I don't have much knowledge and experience about code architecture for games are, yet. I think I will go with saving state every Xth number of turn. \$\endgroup\$ Jul 22 at 10:55
  • \$\begingroup\$ @user7888262 The snapshot can reduce the risk of having bugs when replaying the game (mainly those introduced by floating point related issues), but you'll have to figure of a way to also mitigate this. E.g. you store the velocity and acceleration of a Grunt in snapshot s45, and so for frames between s45 and s46 you apply the velocity and the acceleration. At snapshot s46, the Grunt is expected to be at position (34.0, 67.5), but due to floating point errors introduced, it is now at (34.2, 68.4). (cont...) \$\endgroup\$
    – Vaillancourt
    Jul 22 at 13:49
  • \$\begingroup\$ You could just "replace" it to the correct position at the frame of s46, but it would appear to "jump" or to "teleport" slightly, causing visual glitches. So you'll need to figure ways to smooth the transition. The best tip here is really "first do something that works and later if you have problems - upgrade". Start small with a prototype. Get to know that beast, because that is really what it is. \$\endgroup\$
    – Vaillancourt
    Jul 22 at 13:49
  • \$\begingroup\$ Gotcha.. thanks much @Vaillancourt \$\endgroup\$ Jul 22 at 16:18
1
\$\begingroup\$

TL;DR

Go with the simplest, most robust solution until it becomes a problem.

Saving the entire game state

From my understanding game is turn-based, with a super finite amount of actors and actions on a small map at anytime. I can almost guarantee that it will never be a problem saving your entire game state into a single Firestore document. So go with the simplest, least bug prone solution. You're a single dev, your resources are super precious and should be spent elsewhere, on the actual game. Saving the entire game state over saving just the deltas or actions between turns might actually represent an economic optimization and save you money, this is because the Firestore monetization model is largely based off document reads. So if you want to go see what the end state of a 58 turn match looked like, instead of having to read 58 documents in order and be charged for each read, you can just get a single snapshot of the 58th turn instead.

If this becomes a problem

If this ever becomes an insufficient strategy due to memory limitations, or whatever else, then it's no big deal to move to a command-pattern (or saving deltas) based solution later, as underlined in @Tooster's response. Now at least you'll know that you're not wasting your time, over-engineering a solution which never had to be. I doubt this will be the case for your game though, and if your priority is the game and not the programming, then I would strongly recommend only doing once you prove that the simpler, more robust solution won't be sufficient.

Resources

Jon Blow has a good talk in which he talks about how he did rewind in Braid. A very different game with very different problems. Nevertheless his solution was pretty straight forward, and because of that he avoided having to constantly squash bugs in his rewind system, and could devote his resources elsewhere.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much. I got a better understanding now. I will watch that rewind talk in the link u shared above. \$\endgroup\$ Jul 22 at 11:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.