This is the ninth tutorial in a series about Object Management. It adds support for modular behavior to shapes.

This tutorial is made with Unity 2017.4.12f1.

Behavior Components

Currently, all shapes move and rotate, but that's not the only thing that they could do. We could come up with different behavior that we'd like shapes to exhibit. To make shapes do something else, we just have to add code for it to Shape.GameUpdate . But if we define lots of behavior, then that method would become quite large. Also, we might not want all shapes to behave the same. We could use toggles to control what a shape does, but that would bloat Shape with toggles and configuration options for all possible behavior. Ideally, the behavior is modular and can be defined in isolation. That's exactly what Unity's MonoBehaviour offers, so it makes sense to implement each behavior pattern as its own Unity component.

Abstract Behavior Create a new ShapeBehavior component script and have it extend MonoBehaviour , as usual. This will be the base class for our behavior, which we'll extend with concrete behavior, like movement. The base ShapeBehavior type shouldn't be instantiated, because it doesn't do anything on its own. To enforce this, mark the class as abstract . Why not name it ShapeBehaviour ? Unity uses the British spelling for its MonoBehaviour class, which deviates from the otherwise consistent usage of American spelling. We're defining our own behavior base, so I stick to the American spelling. using UnityEngine; public abstract class ShapeBehavior : MonoBehaviour {} Just like with Shape , we won't rely on separate Update methods but instead use our own GameUpdate method, so add it to ShapeBehavior . But ShapeBehavior just defines common functionality, not an actual implementation. So we'll only define the method signature, followed by a semicolon instead of a code block. That defines an abstract method, which has to be implemented by classes that extend ShapeBehavior . public void GameUpdate (); Abstract methods must be defined as such explicitly, with the abstract keyword. public abstract void GameUpdate (); Also, the behavior acts on a shape, so we'll add one as a parameter. That way we don't have to keep track of it with a field. public abstract void GameUpdate ( Shape shape ); Besides that, each shape behavior will probably have configuration and state, which we'll have to save and load. So add abstract Save and Load methods too. public abstract void Save (GameDataWriter writer); public abstract void Load (GameDataReader reader);

Movement Our first concrete shape behavior component will be about simple linear movement. It'll function exactly like the movement that we currently have, just implemented in a separate class. Create a MovementShapeBehavior script that extends ShapeBehavior . It needs a Velocity vector property that it uses in GameUpdate to adjust the shape's position, and it must save and load it too. using UnityEngine; public class MovementShapeBehavior : ShapeBehavior { public Vector3 Velocity { get; set; } public override void GameUpdate (Shape shape) { shape.transform.localPosition += Velocity * Time.deltaTime; } public override void Save (GameDataWriter writer) { writer.Write(Velocity); } public override void Load (GameDataReader reader) { Velocity = reader.ReadVector3(); } }

Rotation Do the same for rotation, creating a RotationShapeBehavior class that rotates with an AngularVelocity vector property. using UnityEngine; public class RotationShapeBehavior : ShapeBehavior { public Vector3 AngularVelocity { get; set; } public override void GameUpdate (Shape shape) { shape.transform.Rotate(AngularVelocity * Time.deltaTime); } public override void Save (GameDataWriter writer) { writer.Write(AngularVelocity); } public override void Load (GameDataReader reader) { AngularVelocity = reader.ReadVector3(); } }

Adding Behavior When Needed In SpawnZone.SpawnShape , add these behavior components to the shape and set their properties, instead of the properties of the shape itself. public virtual Shape SpawnShape () { … var rotation = shape.gameObject.AddComponent<RotationShapeBehavior>(); rotation .AngularVelocity = Random.onUnitSphere * spawnConfig.angularSpeed.RandomValueInRange; Vector3 direction; switch (spawnConfig.movementDirection) { … } var movement = shape.gameObject.AddComponent<MovementShapeBehavior>(); movement .Velocity = direction * spawnConfig.speed.RandomValueInRange; return shape; } Is it acceptable to use var here? There are no hard rules about when to use var instead of an explicit variable type, as long as the compiler can figure it out. My rule of thumb is that the type should be mentioned explicitly somewhere in the assignment. A constructor method invocation is the best example, but I also consider AddComponent<RotationShapeBehavior> explicit enough. A benefit of using components for isolated bits of behavior is that we can omit them when they aren't needed. That way we can avoid some unnecessary work. In the case of movement and rotation, we only have to add their behavior if they would have a nonzero speed. float angularSpeed = spawnConfig.angularSpeed.RandomValueInRange; if (angularSpeed != 0f) { var rotation = shape.gameObject.AddBehavior<RotationShapeBehavior>(); rotation.AngularVelocity = Random.onUnitSphere * angularSpeed ; } float speed = spawnConfig.speed.RandomValueInRange; if (speed != 0f) { Vector3 direction; switch (spawnConfig.movementDirection) { … } var movement = shape.gameObject.AddBehavior<MovementShapeBehavior>(); movement.Velocity = direction * speed ; } If the spawn zone has a speed range from zero to some nonzero value, then it is extremely unlike that we'd end up with a speed of zero. But if the spawn zone's speed range is set to zero—because we didn't want any movement or rotation at all—then the behavior will always be omitted. Shape with movement but without rotation.

Adding Behavior We're now adding the required components to shapes, but they've stopped moving and rotating. That's because we're not invoking the required GameUpdate methods yet. That's the responsibility of Shape , and to do so it will need to keep track of its behavior components. Give it a list field for that purpose. using System.Collections.Generic; using UnityEngine; public class Shape : PersistableObject { … List<ShapeBehavior> behaviorList = new List<ShapeBehavior>(); … } Next, we need a method to add a behavior instance to the shape. The most straightforward approach is a public AddBehavior method with the behavior as a parameter, which adds it to the list. That method has to be invoked either before or after adding the component to the shape's game object. public void AddBehavior (ShapeBehavior behavior) { behaviorList.Add(behavior); } We can make this more convenient by moving the AddComponent invocation inside the AddBehavior method, having it return the new behavior. To make that work, we have to turn AddBehavior into a generic method, just like AddComponent . That's done by attaching a type placeholder to the method name, between angle brackets. The placeholder name doesn't matter but is usually named T as a shorthand for template type. public T AddBehavior <T> () { T behavior = gameObject.AddComponent<T>(); behaviorList.Add(behavior); return behavior; } However, it only works when AddBehavior is used with a type that extends ShapeBehavior . To enforce that constraint, write where T : ShapeBehavior after the method name. public T AddBehavior<T> () where T : ShapeBehavior { … } Now we can simply replace AddComponent with AddBehavior in SpawnZone.SpawnShape . var rotation = shape. AddBehavior <RotationShapeBehavior>(); … var movement = shape. AddBehavior <MovementShapeBehavior>(); Finally, we can remove the old code from Shape.GameUpdate and instead invoke the GameUpdate method of all its behavior, with itself as the argument. That will make the shapes move and rotate again. public void GameUpdate () { //transform.Rotate(AngularVelocity * Time.deltaTime); //transform.localPosition += Velocity * Time.deltaTime; for (int i = 0; i < behaviorList.Count; i++) { behaviorList[i].GameUpdate(this); } }

Removing Behavior Adding behavior each time we spawn a shape works fine when instantiating new shapes, but leads to duplicate behavior components when shapes get recycled. Behavior duplicates. The quickest way to fix this is to simply destroy all behavior and clear the list when a shape is recycled. That means that we'll be allocating memory even when reusing shapes, but we'll deal with that later. public void Recycle () { for (int i = 0; i < behaviorList.Count; i++) { Destroy(behaviorList[i]); } behaviorList.Clear(); OriginFactory.Reclaim(this); }

Saving When saving a shape, we now also have to save all its behavior. That means that we change our save file format, so increase Game.saveVersion to 6. const int saveVersion = 6 ; Just like with the list of shapes, we have to save the type of each behavior in the list. Once again, we can use an identifier number for that. But this time we're dealing with class types, not prefab array indices. We have a fixed amount of behavior types, two at the moment. Let's define a ShapeBehaviorType enumeration to identify movement and rotation, put in its own script file. public enum ShapeBehaviorType { Movement, Rotation } Next, add an abstract BehaviorType getter property to ShapeBehavior , so we can get a hold of the correct enumeration value. public abstract ShapeBehaviorType BehaviorType { get; } The implementation of the property is simple. MovementShapeBehavior always returns ShapeBehaviorType.Movement . public override ShapeBehaviorType BehaviorType { get { return ShapeBehaviorType.Movement; } } And RotationShapeBehavior always returns ShapeBehaviorType.Rotation . public override ShapeBehaviorType BehaviorType { get { return ShapeBehaviorType.Rotation; } } Now we can write the behavior list in Shape.Save . For each behavior, first write its type, cast to an integer, then invoke its own Save method. That replaces the writing of the old movement and rotation data. public override void Save (GameDataWriter writer) { … //writer.Write(AngularVelocity); //writer.Write(Velocity); writer.Write(behaviorList.Count); for (int i = 0; i < behaviorList.Count; i++) { writer.Write((int)behaviorList[i].BehaviorType); behaviorList[i].Save(writer); } }