We have a server and a database in the cloud. We have a game where each player can grow a farm on a massive x,y grid. Each hour is a grow cycle. All players can harvest anyone's plants. Player A plants some seeds and logs off. Player B logs in five hours later and harvests Player A's plants. At 3:00 PM there is a global rain on the x,y grid that gives plants a bonus grow cycle.
scenario 1: The server timestamps each players actions and the global rain adds a counter to global effects, which is read when the player logs in and applied to the area they see, this effect can only happen once per day and removed by nightly maintenance.
Player A: Plants seeds 1:00 PM. (Server timestamps plant entities with 1:00 PM) and logs off. Player B: Logs on harvests seeds at 6:00 PM (Plants grow cycle is 5 + 1 (rain)) = 6. (the grow cycle is calculated when Player B sees the plants in his view area)
scenario 2: The server does global updates on all entities.
Player A: plants seeds 1:00 PM. (no time stamp). Logs off grow cycles occur each hour: 2:00 PM +1, 3:00 PM (rain) +2 etc.
Player B: Logs on and harvests the plants.
Scenario one seems like the best case, with also checking the view area around the player and updating plants on the hour or after a rain so the player that is staying logged in will see the plants grow.
I feel like I am missing something, such as a global save on server shutdown.
Are both scenarios useful for different functions?
Answer
Both scenarios are useful, there are object that will require to be implemented in the second scenario. Yet, I suggest to use the first one as much as possible.
Scenario 1
Using scenario 1 falls down to the problem of trusting the client. As you know, trusting the client to do computation is not advised, because the user may tamper with the client.
There is one exception: You can let the client compute what the client sees. Here are two common examples:
If the client has high latency, you don't want it to be always waiting for the server response for every player input... instead program the client to send the input to the server but also present to the player a predicted result (if possible). When the server responds you can present to the player the actual result. The side effect is that client may see thing suddenly jump when the client shows the update from the server. As far as the server is concern, the input sent by the client is what matters, it doesn't need to worry if the client sees something different※.
If there is a long process that the player has to wait, the client can present to the player how much time is left... you would also program the client to check again with the server, you may check at given intervals or you may check only when the time (according to the client) reaches zero. As far as the server is concerned, that is irrelevant, what matters to the server is that at some point the client requests the status of process, the server checks the time, decides if it is completed or not and responds.
※: If line of sight is very important for the game, you may consider to do line of sight checks for the player in the server (that will prevent the player from looking at something that shouldn't). But in practice handling line of sight checks for all the players in an mmo is (often) too much work for the server. So, keep in mind that you may need some compromise between playability and performance on one hand and preventing cheats on the other.
Now, you have your player that walks into an area where there are these plants you describe. The client will query the server for the objects in this area, and the server will send the plants.
Now, it doesn't really matter if, by tampering with the client, the player sees the plants in a wrong state. So we may let the client compute the state of the plants. That means that the server may send to client the timestamp of the last update of each plant and let the client figure it out.
Handling randomness in the Scenario 1
I have seen developers who think that the reason to implement the second scenario is randomness... that is, for example, if rain is random then the client will have no way to tell if there was rain in this area, therfore the server will have to handle that...
Well, yes and no.
No: you can let the server set a seed for the particular random event and send that to the client. Now, the client can simulate the elapsed time using that random seed and (if the client has not been tampered with) it should arrive to the correct result.
Yes: if you send the seed to the client, then a malicious user may be able to use it to predict how the simulation will go in the future. You might not want this, as it may give away exploitable information... for example it might reveal that certain monster will appear in certain location at certain time.
So, you shouldn't use this solution for everything.
Besides, if the period of time that needs to be simulated is too long, the client may have to do a lot of work and that may be problematic. You can mitigate that by doing updates in the server (like in the scenario 2) just not that often, for example you may update once per day, an let the simulation of hours to the client. This may also allow you to change the seeds for random events every day, preventing malicious users to get information too far ahead in the future.
Handling persistence in Scenario 1
You will persist on the actions of the players anyway. For example, if the player got an item, you want to persist that to pernament storage, regardless of which implementation you are using.
Similarly, if the player starts a process that will take X time, the game can store that the process started at such and such timestampt and that will take X time. There is no need for consecutive storage of the progress of the process.
Both scenarios are equally susceptible to disk failures, so that won't be a difference either.
From the point of view of persistence, both scenarios work.
Scenario 2
I'll be taking Minecraft as example. Minecraft (at least old versions, prior being acquired by Microsoft) exists purely on the scenario 2.
This is why when you sleep the pork chop in the furnace, it doesn't cook. Mods that change that, do so by sending ticks to nearby objects when you go to bed. Yet, if the objects in Minecraft could receive an elapsed time instead of a "tick", the game could just tell the furnace that X amount of time has passed and then the code in the furnace would figure out how much cooking was done in that time. Furthermore, the game wouldn't have to do that for all the loaded chunks, just those visible by a player.
The scenario 2 is also how plant growth works in Minecraft. Saplings will take ticks, and after a given amount of ticks they grow. When you plant a sapling, it will be eligible to receive ticks from the chunk where it is. Not all items eligible to receive ticks from the chunk will, only a random subset. Because of this randomness the plants don't all grow at the same time, and that makes it harder to implement them in the scenario 1...
But not impossible. There are three solutions:
The game quickly simulates all the ticks that correspond to the time that has passed. That is, in a loop, being each iteration one of the ticks that should have happened in the time that has gone by... decide if it was be received, and if it was then send the tick.
Given that for each tick there will be a given chance that the sapling will get it, we know that for a large number of ticks the sapling would have got a proportion that converges to chance of getting a tick (e.g: If a sapling gets a tick with a 20% chance, for a large enough number of ticks, the sapling would have got approximately 20% of them)... so we can make an educated guess of the number of ticks received and apply that.
Simulate beforehand, so that you can convert the process into one that you know when will be completed. In this case, when planting the sapling the game will do a loop, and in each iteration check with a random if the tick will be received, see how many iterations it takes to get enough ticks and convert that to the time needed to grow. Store the time needed to grow... now when the player looks at it, the game can take the timestamp of when it was planted and the total time to grow and decide how much has grown so far.
Of course, in this case all this rushed simulation will be done in the server so that it can be persisted and to prevent any possible discrepancies between clients.
The advantage of Scenario 2
You can pause.
In scenario 1, you don't need extra effort to update the world. But you would have to do a global update (like those done in scenario 2) to be able to pause the world. That is because when the game checks the time, it will see that some have gone by.
To correctly pause the world, you need to take the time for the pause, go over each entity and compute the progress for that time, update its state it and mark it in pause. To resume, you need to take the time again, and go over each entity again, remove the pause mark and set the start time to time taken.
On the other hand, in scenario 2, you only need to stop updating. So, in scenario 2 you get the pause for free, but updating takes work. In Scenario 1 you get the updates for free but pausing takes work.
You might still need Scenario 2
Finally, there are things that need to be handled purely in the scenario 2. These are things that when completed will trigger another process or will require interaction with an external system.
A process A that starts another process B can be tricky to implement. You can do that, if the process A will start another process B in the future, you can set the starting time of the process B at the time when the process A will be completed. You can chain multiple processes this way.
If you also need processes to cancel other processes, that's harder. For example the process C will cancel the process D, then you would need to store the cancellation time for D, compare it to its completion time... and if it will be cancelled, then any effect the process D completing should be removed (for example if the process D was supposed to start the process E, then that should no longer happen).
The problem above gets even worse if the dependencies of the processes have interlaced branches (e.g. process A starts process B and C, process B will cancel process C, and process C will cancel process B).
Because of those complication, you might decide that it is better to implement that on the scenario 2.
The other example where you need scenario 2 is when you need to interact with an external system when the player is not logged in. For example, if the player let something crafting in your game, and you want the game to send a message to the player telling that it is completed... even when the player is not logged in. There is no way around it, you will have to implement that using the scenario 2.
Conclusion
- you can implement everything in scenario 2 (like Minecraft does)
- you may also use scenario 1 as an optimization, letting thing work in scenario 2 when there is a player present
- there are things that can be implemented only in scenario 2
No comments:
Post a Comment