Game view/logic and MVC confusion

    This site uses cookies. By continuing to browse this site, you are agreeing to our Cookie Policy.

    • Game view/logic and MVC confusion

      Hello guys.

      I've recently bought the book and reading it at the moment and have couple of questions already.

      At the very high level book mentions that there should be separation into 3 main layers: application, game view and game logic. That being said, there was a note that this is like mvc plus application layer on top. After that there goes brief description of what other two layers contain.

      More exactly, game view is said to be responsible for rendering, checking controls and sending commands. Here is where my confusion starts. In classic MVC AFAIK the C part, which is controller, is responsible for doing input fetch and command sending to the model. But here the V and C are merged together.

      For example, I have simple character which can just WalkTo(point). This is my model (game logic). I want it to talk while walking (V part of the MVC should be responsible for that). And a character could be controlled by the human player or by some bot brain (C part of the MVC).

      However there are no controllers in the book and I'm a bit lost actually. Can anyone enlighten me why there are only two separate modules instead of three?

      --

      Another a bit unrelated question is about the input. Author says that invoking model api directly is a bad bad thing and that one should prefer sending commands. This looks like another pattern - command pattern if I understand it right. It is also said that the benefit is in the ability to plug in the console from which you can send those commands.

      I do not really understand the idea. If I have

      if (button is pressed)
      {
      model.WalkTo(...);
      }

      in the controller, why can't I do the same from the console parser? Which looks like just another controller for me btw! The command pattern AFAIK is used for undo/redo and buffering the commands but here we don't need both.

      So again, what of the purpose of introducing another layer of indirection into input pipeline?

      Thanks in advance, the book really gave me a bunch of thoughts on the game engine and I suppose it's only the beginning :)
      Looking for a job!
      My LinkedIn Profile
    • Essentially the 'view' in GCC is both a view and a controller, I look at it like Qt's model-view system, you can have one data set, and represent it in several different views (list view, tree view, table view)
      PC - Custom Built
      CPU: 3rd Gen. Intel i7 3770 3.4Ghz
      GPU: ATI Radeon HD 7959 3GB
      RAM: 16GB

      Laptop - Alienware M17x
      CPU: 3rd Gen. Intel i7 - Ivy Bridge
      GPU: NVIDIA GeForce GTX 660M - 2GB GDDR5
      RAM: 8GB Dual Channel DDR3 @ 1600mhz
    • Correct, the tradition view & controller are merged. The real purpose here is so that the model (game logic) doesn't know whether the attached view is a player or not. Some games do separate out then keyboard and UI input into a separate controller system. Our input scheme is simple enough that the layer of complexity wasn't necessary. If like the extra layer of abstraction that a controller brings, feel free to add one.

      You're correct that the event system is similar to the command pattern, though it's not exactly the same. The real purpose of the event system is not creating undo buffers, it's decoupling systems from each other. If the view had your code, then the view would be directly tied to the model. Every time you changed the WalkTo() function's interface, you'd have to change the view as well. This isn't a big deal if you only have one place that calls the function, but what if you have 10? Or 100? This can quickly become cumbersome.

      The example I like to use is Sim death in The Sims Medieval. When a Sim dies, a number of systems have to be notified. The role system, the scheduler, the object manager, and a number of other systems need to know that the Sim is now a fading memory. We could have written a bunch of code to notify each system directly inside the death system. If we did that, it means that we'd have to change the death code every time one of those systems needed a new way of handling it.

      Even worse, what happens if you have a system that care about it only at certain times? The Role system doesn't care unless this is a role Sim. That means we'd need to either have a flag on the Sim or search through the hash of Sims to see if this Sim was a Role sim.

      Instead, we just threw an event with a reference to the Sim and (I think) the method of death. Any system that cared about it registered for the event. So when a Sim became a Role Sim, the system registered for that Sim's death event.

      The entire purpose of design patterns and code abstraction is to decouple systems and allow them to change independently. You should strive to decouple systems that really have no business knowing about each other.

      -Rez


    • You're correct that the event system is similar to the command pattern, though it's not exactly the same. The real purpose of the event system is not creating undo buffers, it's decoupling systems from each other. If the view had your code, then the view would be directly tied to the model. Every time you changed the WalkTo() function's interface, you'd have to change the view as well. This isn't a big deal if you only have one place that calls the function, but what if you have 10? Or 100? This can quickly become cumbersome.

      -Rez


      I see that idea of separation does make perfect sense here.

      However I've one note on interface changing. If I change event semantics and add for example second parameter (equal to MoveTo() interface change) I will still need to change all places which use that event (i.e. sending and receiving), right? I just don't see how separation helps to solve change issue.

      In the book there was the example with car which had emergency break command with 3 params. I suppose it maps to the event with 3 params as well. So if I would have added 4th parameter then handling the event would change as well.

      As far as I understand at the moment in both cases (events/direct calls) changes are inevitable. Seems like I'm missing something!
      Looking for a job!
      My LinkedIn Profile
    • Of course, you're not going to get around changing both systems if the interface between them is changing as well. There are three components: there's one system, the second system, and the contract between those two systems for how they communicate. This contract can take many forms. The simplest is to have public functions that each system exposes to each other. A more complex solution is using this event system. The concept of the contract is the same, but the advantage of the event system is that neither system explicitly knows about the other. As long as the contract stays the same, the two systems can change independently.

      To further my previous example, let's walk through a typical design flow for something like death. Let's go with the naive implementation first:

      Source Code

      1. class Sim
      2. {
      3. // (pretend there's a bunch of stuff here.... the Sim
      4. // class ended up having over 10,000 lines of code on
      5. // The Sims Medieval.
      6. enum DeathReason = { Fire, Drown, Starve }; // I forget all the reasons, but you get the drift
      7. // The first version is just a function:
      8. void KillSim(DeathReason reason);
      9. };
      10. void Sim::KillSim(DeathReason reason)
      11. {
      12. // remove sim from various systems
      13. if (HasCareer())
      14. Services::GetCareerService()->RemoveSim(this);
      15. if (HasSkill(Skill::CHESS))
      16. Services::GetChessRankingService()->RemoveSim(this);
      17. // Code for actually handling the death of a Sim would go
      18. // here. Stuff like removing from the household.
      19. }
      Display All


      So far, this seems simple enough. And if this were the entire implementation, it totally would be. However, as more systems come online, that block inside KillSim() would get bigger and bigger. Furthermore, the Sim class now knows about the chess ranking service as well as the career service, which may not have been true in the past.

      Now, let's say that we change the way chess rankings work. Instead of a chess ranking service, we now have a skill service that manages the rankings of all skills. It's very easy to write that code and forget to update the KillSim() code, leading to edge cases and bugs. Maybe it only matters for certain skills, so does the new skill manager get a single call from the Sim, or does the Sim handle knowing which skills he needs to update when he dies? What happens when we add a role system? What about a scheduler? What about a meta autonomy service? What happens if the Sim is in the middle of making an AI decision and has to be removed from that queue as well? In each of these cases, it requires yet another line in the KillSim() block:

      Source Code

      1. void Sim::KillSim(DeathReason reason)
      2. {
      3. // remove sim from various systems
      4. if (HasCareer())
      5. Services::GetCareerService()->RemoveSim(this);
      6. if (HasSkill(Skill::CHESS))
      7. Services::GetChessRankingService()->RemoveSim(this);
      8. // autonomy
      9. AutonomyService* pAutonomyService = Services::AutonomyService();
      10. if (pAutonomyService->IsInQueue(this) || pAutonomyService->IsPending(this))
      11. pAutonomyService->Remove(this);
      12. // role system
      13. RoleService* pRoleService = Services::GetRoleService();
      14. if (pRoleService) // may not exist
      15. {
      16. if (pRoleService->IsRoleSim(this))
      17. {
      18. pRoleService->RemoveSimFromRole(this);
      19. // if we have a role, we might have a schedule
      20. SimScheduler* pScheduler = pRoleService->GetScheduler();
      21. if (pScheduler)
      22. pScheduler->RemoveSim(this);
      23. }
      24. }
      25. if (IsInCAS())
      26. Services::GetCASService()->ExitCAS();
      27. // Code for actually handling the death of a Sim would go
      28. // here. Stuff like removing from the household.
      29. }
      Display All


      Code like this is completely unmaintainable and doesn't scale at all. In reality, we had three times as many systems that cared about sim death than the ones I'm showing above. If we implemented death in this manner, I'm sure I'd still be fixing bugs with it.

      And it gets better. What happens when we realize that sim death can't be a simple function and there needs to be a whole death system with multiple interactions? Now THAT system needs to deal with all this stuff, so you get to copy & paste all that code into the new system. Having a Sim know about roles and schedules is reasonable, but the death system should have no knowledge of the role system, sim scheduler, CAS (create-a-sim), or anything else. It shouldn't even know those systems exist. If I delete the role system, I shouldn't have to touch the death code at all. Furthermore, if the death system changes to be something else, it shouldn't effect anything except that system.

      The core idea is to create little black boxes and hook them up with very thin and heavily enforced interfaces. All communication between systems goes through a strict contract.

      Now, enter events. The above code becomes this:

      Source Code

      1. void Sim::KillSim(DeathReason reason)
      2. {
      3. // Create an event with the sim and the reason of the death
      4. DeathEvent* pDeathEvent = new DeathEvent(this, reason);
      5. Services::GetEventService()->QueueEvent(pDeathEvent);
      6. // Code for actually handling the death of a Sim would go
      7. // here. Stuff like removing from the household.
      8. }


      Man, that's a LOT better. And the code never needs to change unless death itself needs to change (such as moving it into a new system) or the contract has to change (maybe we add the object that actually killed the sim rather than just the reason). The role system, the scheduler, CAS, and every other system just registers for the event. The only thing they know about death is what's in the contract. They know which Sim died and the reason for that death. They don't know nor do they care what the death code is doing or even which system is sending the event. The Sim could do it, or a death system could do it. You could even hook up a cheat that just sends this one event.

      So, in conclusion, a single event allowed me to do the same thing while decoupling all these systems together and in less code. Events do have their drawbacks of course, such as ease of debugging and having a slight extra memory & performance overhead, but the rewards are absolutely worth it in the right situations.

      -Rez