Component Architecture II

Czech technical University in Prague
Faculty of Information Technology
Department of Software Engineering
© Adam Vesecký, MI-APH, 2019

Game Architectures summary

Object-oriented approach

Object-oriented approach

  • simple
  • fast prototyping from scratch
  • low overhead
  • easy to debug
  • hard to maintain
  • hard to scale
  • not flexible
  • game objects may have features they don't need

Component-oriented approach

  • attributes are DATA, components are  CODE communicating by means of messages
  • entity is just an empty shell

Component-oriented approach

  • scalable
  • data-oriented
  • components are easy to reuse
  • easy to make new object types
  • polymorphic operations for components
  • dynamic typing - everything is assembled at runtime
  • all dependencies have to be wired together
  • code must be written in an utterly generic way
  • refactoring may become very difficult
  • harder to debug

Hybrid approach

  • tries to get the best of both world
  • necessary for more complex games
  • higher coupling

Communication

Communication practices

By modifying the container object's state

  • e.g.: shared state machine
  • indirect communication
  • difficult to debug

By direct calls

  • OP way
  • fast, but increases coupling
  • e.g.: group of components that are always bound together

By messaging systems

  • events and commands
  • each component can declare interest in relevant messages
  • slower than the direct call, but that cost is negligible in all but performance-critical code
  • difficult to debug, messages can fall into an infinite loop
  • can be implemented via polymorphism, arrow functions,...
  • e.g.: game-over event

Messaging system

  • Components should be notified of any state change that are relevant to them
  • Can be used for returning values (danger of feedback deadlock )
  • Blind event forwarding - an object can forward an event to another object
  • One handler - OnMessage() method, implemented in each component
  • Processing can be instant or delayed
  • Event Queue - pattern for batch message processing, can post events with a delay

Example: Rotation component

class RotationAnim extends Component {

    onInit() {

        this.subscribe("STOP_ROTATION");

    }

 

    onMessage(msg: Message) {

        if (msg.action == "STOP_ROTATION") {

            this.finish();

        }

    }

 

    onUpdate(deltaabsolute) {

        this.owner.rotation += delta;

    }

}

 

// alternative way by using some tricks

new GenericsComponent("RotationAnim")

  .doOnMessage("STOP_ROTATION", (cmpmsg=> cmp.finish())

  .doOnUpdate((cmpdeltaabsolute=> cmp.owner.rotation += delta);

Message types

Unicast

  • a component sends a message to another component
  • in most cases this can be handled by a direct call
  • example: kill an object

Multicast

  • a) component sends a message to specific subscribers
  • b) component sends a message to all objects that meet specific criteria
  • example: notify all nearby units that an enemy has entered the area
  • example: a unit was destroyed -> notify everyone interested

Broadcast

  • rarely used (observer pattern doesn't stick to it)
  • usually for System-Entities communication
  • example: level completed, game over, player died

Example: Messages

Downwell

Engine messages

  • game object state changed
  • game object added
  • game object removed
  • game paused
  • animation ended
  • collision occurred

Game messages

  • gem collected
  • enemy spawned
  • player hit
  • enemy died
  • level completed

Example: Atomic GE Event passing

void Object::SendEvent(StringHash eventType, VariantMap& eventData) {

    SharedPtr<EventReceiverGroup> group(context->GetEventReceivers(this, eventType));

    if (group) {

        group->BeginSendEvent();

 

        for (unsigned i = 0; i < group->receivers_.Size(); ++i) {

            Object* receiver = group->receivers_[i];

            // Holes may exist if receivers removed during send

            if (!receiver) continue;

 

            receiver->OnEvent(this, eventType, eventData);

 

            // If self has been destroyed as a result of event handling, exit

            if (self.Expired()) {

                group->EndSendEvent();

                context->EndSendEvent();

                return;

            }

            processed.Insert(receiver);

        }

        group->EndSendEvent();

    }

}

Example: Godot engine signals

# No arguments.

signal your_signal_name

# With arguments.

signal your_signal_name_with_args(a, b)

 

func _at_some_func():

    instance.connect("your_signal_name", self, "_callback_no_args")

    instance.connect("your_signal_name_with_args", self, "_callback_args")

    

func _at_some_func():

    emit_signal("your_signal_name")

    emit_signal("your_signal_name_with_args"55128)

    some_instance.emit_signal("some_signal")

 

 

Example: Unity Messages

public interface ICustomMessageTarget : IEventSystemHandler

{

    // functions that can be called via the messaging system

    void Message1();

    void Message2();

}

 

public class CustomMessageTarget : MonoBehaviourICustomMessageTarget

{

    public void Message1()

    {

        // handle message

    }

 

    public void Message2()

    {

        // handle message

    }

}

 

// sending message 

ExecuteEvents.Execute<ICustomMessageTarget>(target, null, (x,y) => x.Message1());

 

Example: Unreal Message Bus

  • Facilitates communication between application parts via Message Passing
  • Messages are classified into commands and events
  • All messages are wrapped into IMessageContext, containing additional information

Messaging System Summary

  • Messages are not intended for regular processing
  • If there is something that should run every frame, use polling or direct call
  • Avoid expensive processing in OnMessage handler
  • Separate message from different layers (e.g. collision events from game events)
  • The issue is to decide who is responsible for message handling - a unit, a group or a system?
  • Difficult to revise the messaging architecture once it has been established

Component-oriented game engines

Component-oriented game engines

Artemis framework

Atomic Game Engine

CraftyJS

Unity

Unreal Engine

Godot Engine

CraftyJS

  • Open-Source JavaScript ECS library
  • Game objects (entities) hold their state
  • Components encapsulate behaviors
  • Communication via event callbacks

CraftyJS constructs

// scene initialization

Crafty.e('2D, Canvas, Color, Twoway, Gravity')

  .attr({x: 0, y: 0, w: 50, h: 50})

  .color('#F00')

  .twoway(200)

  .gravity('Floor');

 

// event binding

square.bind("ChangeColor"function(eventData){

        this.color(eventData.color); 

})

// event trigger

square.trigger("ChangeColor", {color:"blue"});

 

// custom component

Crafty.c("Square", {

    initfunction() {

        this.addComponent("2D, Canvas, Color");

        this.w = 30;

        this.h = 30;

    },

    removefunction() { Crafty.log('Square was removed!'); }

})

 

 

Example: Cron

Crafty.c("PowerUp",{

    init:function() {

        this.requires("2D,Canvas,Collision")

        .onHit("PlayerBullet",function() { // destroy powerup on hit

           this.destroy(); 

        })

        .onHit("Player",function(ent) { // apply the effect when player is nearby

            ent[0].obj.trigger(this.effect,this.value);

            this.destroy(); 

        })

        // move the powerUp down

        .bind("EnterFrame",function(){ 

            this.y+=2;

        });

    }

});

 

Crafty.c("Heal",{

    effect:"RestoreHP", value:1,

    init:function(){

        this.requires("PowerUp,heal");

    }

});

Artemis-ODB

  • Java-based ECS framework
  • Android, iOS, HTML5
  • Systems encapsulate logic, components encapsulate data
  • Messaging via reflection or direct call

Artemis-ODB elements

  • World - container for entities, systems and components

WorldConfiguration config = new WorldConfigurationBuilder()

        .dependsOn(MyPlugin.class)

        .with(

            new MySystemA(),

            new MySystemB(),

            new MySystemC(),

        ).build();

 

    World world = new World(config);

 

  • Entity - container of related components
  • Component - pure data class

public class Health extends Component {

   public int health;

   public int damage;

}

 

Artemis-ODB elements

  • Aspect - used for entity systems to tell them which components they should be interested to
    • Aspect.All(types) - system processes entities which have all components of type Aspect
    • Aspect.One(types) - system processes entities which have at least one of the components of type Aspect
    • Aspect.Exclude(types) - system won't process entities which have at least one of the components of type Aspect

// system for three special towers

public RangeTowerSystem() : base(Aspect.All(typeof(Tower)).GetOne(typeof(Ballista),

typeof(Cannon),typeof(MageTower)))

{

 ....    

}

 

Artemis-ODB elements

  • System - encapsulates game logic, operates on a group of entities

public class MovementSystem extends EntityProcessingSystem { 

    public MovementSystem() { super(Aspect.all(Position.class, Velocity.class)); } 

}

  • Archetype - reusable blueprint for new entities

// new archetype

Archetype dragonArchetype = 

    new ArchetypeBuilder()

    .add(Flaming.class).add(Health.class).build(world);

           

// extended archetype

Archetype undeadDragon = 

    new ArchetypeBuilder(dragonArchetype)

    .add(FrozenFlame.class).remove(Flaming.class).build(world);

 

Artemis-ODB elements

  • Transmuter - transforms entity component compositions

 // create new transmuter

this.transmuter = new EntityTransmuterFactory(world)

            .add(FrozenFlame.class).remove(Flaming.class).build();

// apply transformation to entity

this.transmuter.transmute(entity);

  • Plugin - provides extensions as a drop-in plugin

public class MyPlugin implements ArtemisPlugin { 

    public void setup(WorldConfigurationBuilder b) {

        // hook managers or systems.

        b.dependsOn(TagManager.class, GroupManager.class);

        // Optionally Specify loading order.

        b.with(WorldConfigurationBuilder.Priority.HIGHnew LoadFirstSystem());

        // And your custom features

        b.register(new MyFieldResolver());

    }

}

Example: Feed the Space

private static void startBlackHoleGravity(Entity blackHoleE) {

        SpasholeApp.ARTEMIS_ENGINE.getSystem(Box2dWorldSystem.class).setEnabled(false);

        SpasholeApp.ARTEMIS_ENGINE.getSystem(Box2dBodySystem.class).setEnabled(false);

 

        BlackHoleGravitySystem.setBlackHole(blackHoleE);

 

        // add BlackHoleGravitySystem.components

        IntBag actors = Aspects.ACTORS.getEntities();

        for (int i = 0; i < actors.size(); i++) {

            int actorId = actors.get(i);

            if (actorId != blackHoleE.getId()) {

                BlackHoleGravitySystem.components.create(actorId);

            }

        }

 }

Unity

  • Hybrid architecture with messaging system
  • MonoBehaviour can encapsulate both data and logic
  • Scene - contains environment and menu
    • container for GameObject entities
    • positioning via Transform component
  • Prefab - template that stores GameObject along with components and properties

Example: Platformer 2D

Example: Platformer 2D Scene Graph

Example: Platformer 2D Events

// RocketComponent

void OnTriggerEnter2D (Collider2D col) {

    // If it hits an enemy...

    if(col.tag == "Enemy") {

        // ... find the Enemy script and call the Hurt function.

        col.gameObject.GetComponent<Enemy>().Hurt();

        // Call the explosion instantiation.

        OnExplode();

        // Destroy the rocket.

        Destroy (gameObject);

    }

    // Otherwise if it hits a bomb crate...

    else if(col.tag == "BombPickup") {

        // ... find the Bomb script and call the Explode function.

        col.gameObject.GetComponent<Bomb>().Explode();

 

        // Destroy the bomb crate.

        Destroy (col.transform.root.gameObject);

 

        // Destroy the rocket.

        Destroy (gameObject);

    }

}

Example: Platformer 2D delayed invocation

IEnumerator Spawn () {

        // Create a random wait time before the prop is instantiated.

        float waitTime = Random.Range(minTimeBetweenSpawns, maxTimeBetweenSpawns);

        // Wait for the designated period.

        yield return new WaitForSeconds(waitTime);

 

        // Instantiate the prop at the desired position.

        Rigidbody2D propInstance =      Instantiate(backgroundProp, spawnPos, Quaternion.identityas Rigidbody2D;

        // Restart the coroutine to spawn another prop.

        StartCoroutine(Spawn());

}

 

IEnumerator BombDetonation() {

    // Play the fuse audioclip.

    AudioSource.PlayClipAtPoint(fuse, transform.position);

 

    // Wait for 2 seconds.

    yield return new WaitForSeconds(fuseTime);

 

    // Explode the bomb.

    Explode();

}

Godot Engine

Godot Editor

Example: GDScript

bullet.gd:

func _on_bullet_body_enter( body ):

    if body.has_method("hit_by_bullet"):

        body.call("hit_by_bullet")

      

        

enemy.gd:

func _physics_process(delta):

    var new_anim = "idle"

 

    if state==STATE_WALKING:

        linear_velocity += GRAVITY_VEC * delta

        linear_velocity.x = direction * WALK_SPEED

        linear_velocity = move_and_slide(linear_velocity, FLOOR_NORMAL)

 

        new_anim = "walk"

    else:

        new_anim = "explode"

 

func hit_by_bullet():

    state = STATE_KILLED

Component-oriented frameworks

  • ~Libraries and frameworks built on top of existing engines

Unity DOTS

EnTT

Entitas

A-Frame

ECSY framework

var world = new World();

world

  .registerSystem(MoveSystem)

  .registerSystem(RendererSystem);

  

class MoveSystem extends System {

  execute(deltatime) {

    this.queries.moving.results.forEach(entity => {

      let pos = entity.getMutableComponent(Position);

      pos.x += entity.getComponent(Velocity).x;

    });

  }

}

Event-Passing components

Event-passing components

  • ~visual programming
  • thinks solely in terms of sending streams of data from one object to another
  • every component has a set of input ports to which a data stream can be connected, and one or more output ports

Nodus for Unity, deprecated

FlowCanvas for Unity

Event-passing components

Unreal Blueprints

Event-passing components

Pure Data

Event-passing components

Blender 3D

Lecture 4 Review

  • ECS(A) Pattern: the overall behavior of a particular game object is determined by the aggregation of its components and attributes
  • Communication approaches : modifying state, direct call, messaging systems
  • Message Types : unicast, multicast, broadcast
Attributes are DATA, components are CODE

Goodbye quote

It's in the game!EA Sports