Diff-Based Model-View Interaction

Started by
2 comments, last by steven katic 16 years, 1 month ago
A typical implementation of a Model-View-Controller system starts with designing the model, which is generally the game world, and an update function which simulates the model's evolution. As usual, the world contains exactly zero view-related information. Pseudocode:
while true
  world = update(world)
Then comes the view. The most elementary solution would be to generate a brand new view based on the current state of the model. Pseudocode:
while true
  world = update(world)
  graphics = extract(world)
  render(graphics)
This solution is good enough for most business applications, where the view is intended to represent a snapshot of the data along with methods for interacting with that data. As such, generating a brand new snapshot from scratch is sufficient. In the game development world, this isn't enough. First, there is the question of efficiency: extracting a completely new graphical representation of the game world every frame completely ignores the existence of spatial and temporal locality in the game world—even though a given object will have the same aspect and be in the same position from one frame to the other, its aspect and position will be computed every frame. Then, there is the issue of cosmetic time-dependent effects, such as animation or sound: given a snapshot of the world state, what frame of a given animation should be displayed, and what portion of a sound file should be played? Since in almost all cases the world has no reason to store cosmetic information about animations or sounds, these cannot be used in this simple implementation. An improved solution consists in giving the view the possibility to persist some data in-between frames. Pseudocode:
while true
  world = update(world)
  if graphics
    graphics = evolve(world, graphics)
  else
    graphics = extract(world)
  render(graphics)
If no data exists yet, extract creates a reasonable approximation of what the view should look like if it had been present for a short while, based on the world's state. This should generally be a good guess. Then, evolve ensures that the view on a given frame is consistent with the previous frames (animations play the right way, objects don't jump around without interpolation, sounds actually play all the way, and so on). In turn, the evolve function should give results that are close to the guess that would have been made by extract in the same situation, in order to keep the view in sync with the model. While a good improvement (the view now takes advantage of spatial and temporal locality), there are still a few issues to iron out. First, there are the efficiency issues: the evolve function must remove objects which are not there anymore, update those objects that are present, and create avatars for the objects which appears. This means that all objects will be examined (though not necessarily recomputed) even if they haven't changed since the previous step. Then, there are the functionality issues: suppose that, during the update step, a character shot another character. The existence of that shot is not apparent in the world state: the first character's ammo count went down, and the second character's life went down as well. How can the view display the muzzle flash and play the gunshot sound? The evolve function can keep a copy of those variables and perform an educated guess about what happened, but it could be wrong. Or the model could keep a flag about whether the gun was fired, or a list of events which may cause sounds to be played and animations to be played, but this would violate the separation of the model and the view. The problem here belongs with the interaction between the view and the model. Assuming that the model code remained exactly the same, but that in addition of updating the world state, it constructed a diff (a list of differences between the old state and the new state) that the graphics system could examine in addition to the final state, all these problems would be gone. And, looking back from that point, it is fairly obvious that giving the view access to only static snapshots of the model was fairly limiting. Pseudocode:
while true
  world, diff = update(world)
  if graphics
    graphics = evolve(world, diff, graphics)
  else
    graphics = extract(world)
  render(graphics)
However, an issue remains. Here is an example event pseudocode, such as firing a gun:
// Before
fire_gun(args,world):
  world = apply(FIRE_GUN, args, world)
  return world

// After
fire_gun(args,world,diff):
  diff << (FIRE_GUN, args, world)
  world = apply(FIRE_GUN, args, world)
  return (world, diff)
In addition to applying the gun event to the world (something which also happened in the previous version), the event logs itself to the difference list. However, in order for the view to react correctly to this event, it must be aware of the state of the world at the moment when the gun fire occured. Thus, the old world state is attached to the event inside the difference list. Alternately, the view has its own copy of the world state and has it evolve using the received events. Either way, the processing and memory overhead are unnecessarily high. Instead, the system can be reduced to a listener approach, which also serves a nice purpose in the case of networking. Pseudocode:
while true:
  if !graphics 
    graphics = extract(world)
  world, graphics = update(world, graphics)
  render(graphics)
This is not a violation of view-model separation, because update does not know that graphics represents the view: it is just a listener object which will be notified of all events which happen inside the world during the update using a forwarding system:
fire_gun(args,world,listener):
  listener = send(FIRE_GUN, args, world, listener) 
  world = apply(FIRE_GUN, args, world)
  return (world,listener)
By implementing a listener system to extract information from all the events which occur inside the world during an update, a diff-based communication channel from the model to the view appears, and can be used to improve performance and expressiveness of the system. Are there any opinions you wish to share about the above? Past experiences with improving the performance of the model-view-controller approach? Other solutions you may have found to these problems?
Advertisement
Posting because this deserves a bit more time on the front page. I bet quite a few people were like me and typed up a half page affirmation before deciding it wasn't adding much to what you've already said.

Anyway, I think it's a good pattern to use, listening to changes, although I'm betting that most of the people who actually get game dev projects completed don't worry much about model-view separation. If your game design is not extensive enough to require scalable architecture, then MVC can actually complicate your code.

Quote:Original post by ToohrVyk
Then, there are the functionality issues: suppose that, during the update step, a character shot another character. The existence of that shot is not apparent in the world state: the first character's ammo count went down, and the second character's life went down as well. How can the view display the muzzle flash and play the gunshot sound?


While I like the way you connect the model to the view, this part is entirely incorrect. The model has to report more than character position and health. It has to report actions being taken by the character. If all you get out of the updates is that the location is trending to the left how do you know the character is moving left or being pushed to the left. Updates must include *all* state information including character actions. Thus when you receive word of shots fired you must display appropriately.
Something you might find interesting:

http://www.gamasutra.com/view/feature/2280/the_guerrilla_guide_to_game_code.php?print=1

It was a passing interest for me, very readable, and apparent to me is that it very much relates to your discussion, and...in passing: would I be right in saying your "diff concept" serves a similar purpose as the "messaging system" in this article(i.e. another perspective)?

This topic is closed to new replies.

Advertisement