../_images/cover1.gif

Interactive Game Elements & Spawning

We have four kinds of elements that need to be spawned: the Vehicle, the Soldier, the BicycleBuff and EnvObj each has a corresponding spawning class. The spawners and spawned objects inheritance hierarchy can be shown in the system diagram below, in each hierarchy the class has declared some protected method that defined essential functionality patterns that to be utilised and modified by the child classes:

../_images/spawning_inheritance_hierarchy_system_diagram2.png

System Diagram of Spawning Inheritance Hierarchy (ctrl + + to zoom in)

Spawners

The essence of the inheritance shown above is to maximise the reusability of functionalities of the same patterns. In the case of the spawners, all four end-user spawners are attached to the Main Camera which can be shown in the below screenshot:

../_images/spawner_attach.png

They are inherited from the same parent spawner class where the xyz spawning positions and the interval for a custom timer to renew has been defined. These functionalities will be modified and reused in all four end-user spawners. The Soldier and BicycleBuff class have just made modifications on the prefab to instantiate, timer interval and spawn position without changing the functionality pattern thus doesn’t need to be discussed. Here we will only discuss the new things child classes have added when inheriting.

Vehicles

There are two kinds of vehicles, but they behave in the same way, thus we only change the sprite rather than changing the properties of the game object, thus we start with declaring the field variable:

[SerializeField] private GameObject[] _prefabVehicles = default;

Then we modify the original Update function to randomly choose which object to spawn. We also need to modify the timer since when the player is in a buffed state, she’s running 3 times faster, thus vehicles need to be generated 3 times faster:

protected override void Update() {
    if (CustomTimer.Finished) {
        SpawnNewObj(_prefabVehicles[Random.Range(0, _prefabVehicles.Length)]);

        // when in buffed state, spawn the obj at 3 times frequency
        CustomTimer.Duration = PlayerControl.HoriMvtState == HoriMvtState.Buffed
            ? Random.Range(
                ConfigUtils.MinSpawnIntervalObstacle / 3,
                ConfigUtils.MaxSpawnIntervalObstacle / 3)
            : Random.Range(
                ConfigUtils.MinSpawnIntervalObstacle,
                ConfigUtils.MaxSpawnIntervalObstacle);
        CustomTimer.Run();
    }
}

The vehicles will encounter another issue of whether generating in the top lane or bottom lane, this will be handled in the Vehicle script that will be discussed down below.

Environmental Objects

The environmental objects will face the same issue of lane choice as the vehicle does. Since environmental objects are not interacting with the player, we turn to simplify the EnvObj class and squeeze all the functionalities in the environmental objects spawner script.

Same as the vehicle spawner, we declare a list of game objects as prefab pool, but this time we create two key-value pairs to store the random environmental object and lane choices:

// --------------- Serialized Cached References ---------------

[SerializeField] private GameObject[] _prefabEnvObjs = default;

// --------------- Config Params ---------------

private VehicleLane _vehicleLane;

private List<KeyValuePair<GameObject, float>> _envObjs =
    new List<KeyValuePair<GameObject, float>>();

private List<KeyValuePair<VehicleLane, float>> _laneChoices =
    new List<KeyValuePair<VehicleLane, float>>();

In the Start method, we assign each environmental object and lane choices with a certain probability of occurring. This has been actualised using the custom Probability.RandomEventsWithProb method which will be discussed in later sections:

protected override void Start() {
    _envObjs = new List<KeyValuePair<GameObject, float>> {
        new KeyValuePair<GameObject, float>(_prefabEnvObjs[0], 60),
        new KeyValuePair<GameObject, float>(_prefabEnvObjs[1], 20),
        new KeyValuePair<GameObject, float>(_prefabEnvObjs[2], 20),
    };

    _laneChoices = new List<KeyValuePair<VehicleLane, float>> {
        new KeyValuePair<VehicleLane, float>(VehicleLane.Top, 20),
        new KeyValuePair<VehicleLane, float>(VehicleLane.Bottom, 80),
    };

    base.Start();
}

protected override void Update() {
    if (CustomTimer.Finished) {
        // using reusable separate function from Probability Utility class
        SpawnNewObj(Probability.RandomEventsWithProb(_envObjs, 100));

        // when in buffed state, spawn the obj at 3 times frequency
        CustomTimer.Duration = 2;
        CustomTimer.Run();
    }
}

Spawned Objects

The FloatEventInvoker and ZPosChangeable classes have been discussed in previous sections. The most important functionality the SpawnedObj class has declared and can be applied to all children spawned objects is the self destroy functionality where spawned objects destroy themselves when they are too far away from the left boundary of the screen. They will no longer be able to interact with any of the existing game objects in the screen, but they still occupy memory spaces, thus needs to be eliminated:

// when the obstacle is 1.5 screen width behind the player, destroy itself
// setting to 1.5 screen width to avoid bugs caused when deploying on phones
protected virtual void DestroySelf() {
    float xPosSelf   = gameObject.transform.position.x;
    float xPosPlayer = PlayerControl.PlayerTransform.position.x;

    // calculate the x distance between position of obstacle itself and the player
    if (xPosSelf - xPosPlayer < 3 * ScreenUtils.ScreenLeft) {
        Destroy(gameObject);
    }
}

Vehicles

The implementation of the Vehicle class starts with the lane choice:

public enum VehicleLane {
    Top,
    Bottom
}
public class Vehicle : SpawnedObj {
    private Rigidbody2D    _rb2D;
    private SpriteRenderer _spriteRenderer;

    ...

    private VehicleLane _vehicleLane;

The event trigger and self-destroy invoker removal functionalities have been discussed in previous sections, in this section, we only discuss the setting direction according to lane choice functionality.

We first choose the lane by utilising the built-in Random.Range function. Then if the lane choice is top, spawn on top lane range, otherwise, spawn on bottom lane range. We place the vehicle to the corresponding initial position and make the vehicle start moving by adding force onto the rigidbody2D component. Finally, we decide on the sprite direction.

private void SetLaneAndDirection() {
    int enumLen = System.Enum.GetNames(typeof(VehicleLane)).Length;
    _vehicleLane = (VehicleLane) Random.Range(0, enumLen);

    if (_vehicleLane == VehicleLane.Top) {
        transform.position = new Vector3(
            transform.position.x,
            Random.Range(_topLaneBot, _topLaneTop),
            transform.position.z);

        _rb2D.AddForce(new Vector2(100, 0)); // moving towards right

        // flip the sprite horizontally to make the vehicle face right
        _spriteRenderer.flipX = true;
    } else {
        transform.position = new Vector3(
            transform.position.x,
            Random.Range(_botLaneBot, _botLaneTop),
            transform.position.z);

        // add force to initialise the vehicle movement
        _rb2D.AddForce(new Vector2(-200, 0)); // moving towards left

        // don't flip the sprite horizontally to so the vehicle faces left
        _spriteRenderer.flipX = false;
    }
}
Vehicles towards left without sprite flipping
vehicles_towards_left
Vehicles towards right with sprite flipping
vehicles_towards_right

Soldier

Apart from event handling functionalities, we have discussed in previous sections, the interesting part about Soldier class is the chasing functionality.

Initially, the soldier is standing still, as long as the x-position of the main character is bigger than that of the Soldier which means it’s on the right of the Soldier, it will start the chasing:

private void StartChasing() {
    if (!_isRunning && PlayerControl.PlayerTransform.position.x > transform.position.x) {
        _isRunning = true;
        _animator.SetBool("IsRunning", _isRunning);
    }
}

The actual chasing involves calculating the direction from the soldier towards the main character and normalise it. Then adding the force towards the normalised direction to consistently chasing down the player:

private void Chasing() {
    if (_isRunning) {
        _rb2D.velocity = Vector2.zero;

        // calculate direction to the player and moving towards it
        Vector2 direction = new Vector2(
            PlayerControl.PlayerTransform.position.x - transform.position.x,
            PlayerControl.PlayerTransform.position.y - transform.position.y);
        direction.Normalize(); // normalise it to make it a unity vector

        // because we set the speed to zero previously, adding the force with the original
        // impulse force with the normalised direction we have just calculated will
        // make the game object moving at the same speed as before
        _rb2D.AddForce(direction * _impulseForce, ForceMode2D.Impulse);
    }
}

Analogous to the player animation switch, he sprite switching of the solider has been accomplished using the Unity Animator as well. The transition logic between animations is simply actualised by manipulating the IsRunning boolean variable which has been shown in the above functions.

../_images/animator_soldier.png

Unity Animator

Soldier Chasing Elinging
soldier_chasing_elingling