4
\$\begingroup\$

From a data perspective, how decoupled should game logic and rendering be? How much does it have to know about each other?

Take a look at the following example. I'm going to use javascript because that's what I'm most familiar with.

Block

Basically, when the Block.destroy is called, there is a delay before it loses a life

function Block(life, destructionDelay){
    this.life = life;
    this.destructionDelay = destructionDelay;

    this.destructionCountdown = 0;
}

Block.prototype = {
    destroy: function(){
        this.destructionCountdown = this.destructionDelay;
    },

    update: function(){
        if(this.destructionCountdown > 0){
            this.destructionCountdown--;

            if(this.destructionCountdown === 0){
                life--;
            }
        }
    }
}

What was supposed to happen here is when the Block is destroyed, it plays a "being destroyed" animation before it loses a life/destroyed.

As I have been informed, it is ideal for the game logic to know nothing/minimal about the renderer and vice-versa. How do I design these kinds of entities?

\$\endgroup\$
1
\$\begingroup\$

As completely as possible. Each sub-system within the game should be able to operate with minimal dependencies. A popular method is called the Entity-Component-System.

Game example
Suppose there is a drawing function. This would be a "System" that iterates through all entities that have both a physical and a visible component, and draws them. The visible component could typically have some information about how an entity should look (e.g. human, monster, sparks flying around, flying arrow), and use the physical component to know where to draw it.

Another system could be collision detection. It would iterate through all entities that have a physical component, as it would not care how the entity is drawn. This system would then, for instance, detect arrows that collide with monsters, and generate an event when that happens. It should not need to understand what an arrow is, and what it means when another object is hit by an arrow.

Yet another component could be health data, and a system that manages health. Health components would be attached to the human and monster entities, but not to arrow entities. The health management system would subscribe to the event generated from collisions and update health accordingly. This system could also know and then iterate through all entities with the health component, and regenerate health.

In this way, the only difference between a Block and any other entity in your game is the set of behaviors attached to them. Attaching a behavior to Block causes that behavior system to affect all instances of Block the same way.

Using your example, the "health system" would respond to some input (a collision event?) and set the initial "life" (Block.destroy()). When the health system updates, the Block's life is drained. When the Block has no life left, the health system calls Block.DestroyForReal() which removes the dead Block instance from all systems.

As a real-life analogue, it makes no difference what type of machine a gear is installed in. Regardless of what the machine does, an individual gear has only one behavior; to turn. Likewise, when you are driving a car, the engine doesn't worry about things like "low fuel", that is the responsibility of the driver-system. The driver only fills the fuel-system; the engine only empties it.

If I were going to do this in Unity, I'd have a "health" (stop-watch) script and the "animator" (animate model transforms) script. When I click a Block, the health script is attached to the block and initialized. It runs on its' own and, when the count-down is finished, removes itself and adds the animator script. The animator script does nothing but integrate deltaTime into the model's bone-transforms (which the renderer uses). When the animation is complete, the animator script is detached and the Block is disabled/destroyed. The renderer does not care which scripts are attached to the Block and/or what they are doing to it.

Here is the "health-system" for an Arkanoid-style Brick:

using UnityEngine;
using System.Collections;

public class BrickLife : MonoBehaviour
{
    public int InitialLife = 1;
    private const int MaxLife = 5;
    private static Color[] colors = 
       new Color[MaxLife] { Color.red, Color.green, Color.blue, Color.yellow, Color.cyan };

    void Start()
    {
       if (InitialLife == -1) //-1 is indestructible
          GetComponent<MeshRenderer>().material.color = Color.gray;
       else
       {
          InitialLife = Mathf.Clamp(InitialLife, 1, MaxLife);
          UpdateColor();
       }
    }
    void Update() { }
    void UpdateColor()
    {
       GetComponent<MeshRenderer>().material.color = colors[InitialLife - 1];
    }
    void OnCollisionEnter(Collision collision) //When ball hits
    {
       if (InitialLife > 0) //Don't hurt/kill indestructible blocks
           if (--InitialLife == 0) //Hurt the block
               Destroy(gameObject); //If it's dead
           else
               UpdateColor(); //If it's not dead
    }
}

Physics handles the physics, Renderer handles rendering, BrickLife handles brick state/existence.

\$\endgroup\$
3
  • \$\begingroup\$ In your "health system", how do I integrate the rendering system into that? \$\endgroup\$
    – userx01
    Mar 22 '16 at 13:04
  • \$\begingroup\$ @userx01, they aren't integrated at all; the "health system" is just a stop-watch. When the timer expires, the appropriate block is set to isDying. For any block the renderer handles, it is either isDying and gets the animation, or not, and doesn't. Once the renderer completes the death animation, the block is told to become invisible/inactive and/or die, for real. \$\endgroup\$
    – Jon
    Mar 22 '16 at 19:39
  • \$\begingroup\$ @userx01, I added a "real-world" case at the end. The scripts give behaviors to objects so, by removing the timer and adding the animator, the block "becomes" isDying (without actually storing a bool). \$\endgroup\$
    – Jon
    Mar 22 '16 at 19:58
4
\$\begingroup\$

From my experience, there is only a certain level of abstraction you can do before it becomes overdone. It also depends on whether you are using a game engine that intends you to do it that way or not. Unity for one splits up into Update, FixedUpdate and LateUpdate, each with its own intended use.

Regardless, if you are trying to split code into renderer and logic, one would require 2 classes. A logic class and a renderer class. A logic class will send render events to the renderer class and the renderer just do as it is told.

From what I can see in your code, this is all logic code. How I would suggest doing it using the above architecture is (in pseudocode),

Logic.Destroy() {
    Renderer.PlayDestroyedAnimation();
}
Renderer.PlayDestroyedAnimation() {
    start countdown
    if (countdown < 0) Logic.DestroyedAnimationFinished();
}
Logic.DestroyedAnimationFinished() {
    life--;
}
\$\endgroup\$
2
  • \$\begingroup\$ I think this is a good design. The one I came up with is having the "destructionCountdown" duration coincide with the animation duration, then when the destroy() was called, it will both end at the same time. I think your design is much more reliable and graceful though. \$\endgroup\$
    – userx01
    Mar 21 '16 at 2:31
  • \$\begingroup\$ In my view DarkDestry is correct based on own experience. In a current project, I have created a "game engine" that is the logic class. It stores all entities & updates them & their components. Then I have a separate render class that is told what to display (images, animations, timings, etc.) by the game engine/logic class. Render is never authoritative. Even added functionality to track what entities are rendered & which ones are also doing something but out of view. Out of view entities simply skip sending commands to the render class. \$\endgroup\$
    – Krekin
    Jul 26 at 9:24
3
\$\begingroup\$

If you look at the MVC pattern (Model, View, Controller) the answer really becomes clear. The model is your data, the view is the rendered representation of your data. By convention in MVC, your view knows ideally nothing about your model and vice versa. That's where the controller comes in.

The controller is responsible for communication between the model and view. If the user interacts with the view, the message is sent to the controller. If the model needs to be updated from that user interaction the controller passes the model the necessary info and gets back what it needs to update the view (render it again).

It is certainly possible to write games with MVC however most popular gaming frameworks don't use traditional MVC. Instead you'll see the concept again and again of Sprites, Containers and object.update() functions. In this case your view respresentation exists within your models (sprites and containers) and receives events and data with each call to object.update(). Another common idea is that once a sprite is added to a container it becomes "live" and then automatically begins receiving update messages. In this architecture rendering happens completely behind the scenes and is loosely coupled with your data.

For example:

  1. You create 100 enemy sprites
  2. You add them to the game container
  3. In adding them to the game container the game loop begins rendering each sprite on every update loop
  4. Player destroys a few enemies
  5. Enemy sprites remove themselves from the container on death
  6. Update loop no longer renders those sprites because they are removed from the container

As you can see from the above, at no point are the enemy sprites (your data) concerned with rendering themselves.

\$\endgroup\$
2
  • \$\begingroup\$ Lightweight, loop-based MVC is fairly rare. I've written a framework to do that, arc. The separation of concerns is really nice, but by grouping logic primarily by M, V and C you lose the ability to coordinate simultaneous happenings by their position in a single code flow... which is the traditional approach in games. Instead you have to look at a code flow in a controller while looking at separate codeflows in discrete views, even though they may be happening at the same time. There's always a price. But I find it very convenient. \$\endgroup\$
    – Engineer
    Mar 29 '16 at 18:47
  • \$\begingroup\$ @ArcaneEngineer I agree, pure MVC in games is very rare. Although you're likely to see objects and classes in your games "act like" and perform the responsibilities of a Model, View, or Controller e.g. updating data, rendering and listening to events. Most game frameworks I've worked with allow you to plug into an existing update loop and add objects to rendering containers. \$\endgroup\$
    – bideowego
    Mar 30 '16 at 6:38
1
\$\begingroup\$

Game logic and visual representation should be as decoupled as possible. One method is to use an event system. This allows each portion of game logic and each portion of the visual representation to register for events that they care about.

Consider the following psuedo code of your situation.

GameLogic.Register("OnBlockHit", function(...)
{
  FireEvent("OnBeginDestructionCountdown", 10.0);
});

GameLogic.Register("OnBlockDestroyed", function(...)
{
  this.life -= 1;
});

Block.Register("OnBeginDestructionCountdown", function(countdown)
{
  this.dCountdown = countdown;
  this.PlayDestructionAnimation();
});

Block.Register("OnEndDestructionCountdown", function(...)
{
  this.destroy();
  FireEvent("OnBlockDestroyed");
});

// update function
function()
{
  if (this.dCountdown > 0) {
    --this.dCountdown;
    if (this.dCountdown <= 0) {
      FireEvent("OnEndDestructionCountdown");
    }
}
\$\endgroup\$

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.