Bolt 105 - Objects & Arrays

Bolt supports built-in replication of complex types called Objects and Arrays, they are very similar to their plain code C# counterparts, but differ in a few ways. Objects are defined in the Bolt Assets window and can be used as the type for an Array or Object property on a state or another object.

Objects gives you an easy mechanism to encapsulate a group of properties and allows you to implement complex hierarchical data in Bolt that is easily re-usable across different Bolt states. We are going to demonstrate how to use objects inside Bolt by implementing weapon slots for a simple shooter game, we are not going to actually build a game just the data structure and states to replicate the weapons.

Start by creating a new Object in the Bolt Assets window using the right-click menu.

Create a new Object Asset
Create a new Object Asset.

Name the object WeaponSlot, and give it two properties: WeaponId and WeaponAmmo. They should both be integers. As you can see the objects in Bolt do not have properties of their own like the events and states, they are simply containers for data that can be included in other assets.

Configure the new Object Asset
Configure the new Object Asset.

On our CubeState we are now going to add a new property called WeaponsArray that has the type Array, the Element Type set to Object, the Object Type set to WeaponSlot and we're going to give it an Element Count of 3. We're also going to add one more property called WeaponActiveIndex which should have the type Integer. This is for tracking which weapon we have active.

Add WeaponSlot to Cube State
Add WeaponSlot to Cube State.

Important: As always, remember to compile Bolt after we are done with our changes in the Bolt Assets and Bolt Editor windows.

The WeaponsArray property is by far the most complex we have created so far, what Bolt will do is create an array of three 'WeaponSlot' objects for us, which we then can modify and have all of their data automatically replicate over the network.

Note that when we say array here we don't mean an actual C# WeaponSlot[] array, Bolt provides its own array-like type which lets Bolt track changes made to objects easily. The actual type of the WeaponSlots property will be Bolt.NetworkArray_Objects<WeaponSlot>, in general you can mostly ignore this as the normal indexing operations work on this class also and it has the same Length property, etc.

It's time to setup some very simple place-holder weapons for our Cube prefab, drag a copy of the prefab into an empty scene, make sure it's positioned at (0, 0, 0) in the scene. Create a new sphere, capsule and cylinder at (0, 0, 0) and also make them children of the Cube. Remove the default colliders and make sure to disable the three objects, so that they are all off by default. The transform settings I used for them are as follows:

Add Weapon representations to Cube Prefab
Add Weapon representations to Cube Prefab.

This simply gives us an easy way to see which weapon we have active currently. Make sure to apply the changes to the prefab either by hitting Apply at the top of the inspector when the Cube is Selected or simply by dragging in the Cube from the scene into the Cube prefab in the Project window.

We will start to make some modifications to our scripts. At the top of our CubeBehaviour script we are now going to add a new variable called WeaponObjects which is an array of unity GameObject.

using UnityEngine;
using System.Collections;

public class CubeBehaviour : Bolt.EntityEventListener<ICubeState>
{
    public GameObject[] WeaponObjects;
    // ...
}

Go into the inspector for our Cube prefab and drag the three child objects into the WeaponObjects array inspector field.

Drag Weapon object to Cube Behavior Script
Drag Weapon object to Cube Behavior Script.

We're going to add quite a lot of new code to our CubeBehaviour script now, to get our weapons setup properly we need to setup the weapons for each player on the owner, inside Attached. We also need to hook up a callback so we get a notification when the active weapon slot changes.

// ...

public override void Attached()
{
    renderer = GetComponent<Renderer>();

    state.SetTransforms(state.CubeTransform, transform);

    if (entity.isOwner)
    {
        state.CubeColor = new Color(Random.value, Random.value, Random.value);

        // NEW: On the owner, we want to setup the weapons, the Id is set just as the index
        // and the Ammo is randomized between 50 to 100
        for (int i = 0; i < state.WeaponArray.Length; ++i)
        {
            state.WeaponArray[i].WeaponId = i;
            state.WeaponArray[i].WeaponAmmo = Random.Range(50, 100);
        }

        //NEW: by default we don't have any weapon up, so set index to -1
        state.WeaponActiveIndex = -1;
    }

    state.AddCallback("CubeColor", ColorChanged);

    // NEW: we also setup a callback for whenever the index changes
    state.AddCallback("WeaponActiveIndex", WeaponActiveIndexChanged);
}

// ...

There's a new for loop inside the if(entity.isOwner) block which lets us initialize the three weapon slots, here we use the WeaponArray property we defined on our state in the Bolt Assets window. As you can see it functions mostly like a normal array. We randomly assign one of the weapon object models to .WeaponId and then we randomly pick an ammo count between 50 and 100 and assign it to .WeaponAmmo.

We also set the .WeaponActiveIndex property to -1, meaning that we don't have a weapon up by default. The last thing we do is at the end where we add a callback to the "WeaponActiveIndex" property so that we get a notification when the active weapon changes.

The WeaponActiveIndexChanged method looks like this.

void WeaponActiveIndexChanged()
{
    for (int i = 0; i < WeaponObjects.Length; ++i)
    {
        WeaponObjects[i].SetActive(false);
    }

    if (state.WeaponActiveIndex >= 0)
    {
        int objectId = state.WeaponArray[state.WeaponActiveIndex].WeaponId;
        WeaponObjects[objectId].SetActive(true);
    }
}

The first thing we do is to just disable all weapon objects, we then check if our index is >= 0 and if it is we grab the .WeaponId and use that to enable the correct object to show. The last thing we need to do is just polling some standard Unity input in SimulateOwner, like this:

public override void SimulateOwner()
{
    var speed = 4f;
    var movement = Vector3.zero;

    if (Input.GetKey(KeyCode.W)) { movement.z += 1; }
    if (Input.GetKey(KeyCode.S)) { movement.z -= 1; }
    if (Input.GetKey(KeyCode.A)) { movement.x -= 1; }
    if (Input.GetKey(KeyCode.D)) { movement.x += 1; }

    // NEW: Input polling for weapon selection
    if (Input.GetKeyDown(KeyCode.Alpha1)) state.WeaponActiveIndex = 0;
    if (Input.GetKeyDown(KeyCode.Alpha2)) state.WeaponActiveIndex = 1;
    if (Input.GetKeyDown(KeyCode.Alpha3)) state.WeaponActiveIndex = 2;
    if (Input.GetKeyDown(KeyCode.Alpha0)) state.WeaponActiveIndex = -1;

    if (movement != Vector3.zero)
    {
        transform.position = transform.position + (movement.normalized * speed * BoltNetwork.frameDeltaTime);
    }

    if (Input.GetKeyDown(KeyCode.F))
    {
        var flash = FlashColorEvent.Create(entity);
        flash.FlashColor = Color.red;
        flash.Send();
    }
}

The complete CubeBehaviour script now looks like this.

using UnityEngine;
using System.Collections;

public class CubeBehaviour : Bolt.EntityEventListener<ICubeState>
{
    public GameObject[] WeaponObjects;

    float resetColorTime;
    Renderer renderer;

    public override void Attached()
    {
        renderer = GetComponent<Renderer>();

        state.SetTransforms(state.CubeTransform, transform);

        if (entity.isOwner)
        {
            state.CubeColor = new Color(Random.value, Random.value, Random.value);

            // NEW: On the owner, we want to setup the weapons, the Id is set just as the index
            // and the Ammo is randomized between 50 to 100
            for (int i = 0; i < state.WeaponArray.Length; ++i)
            {
                state.WeaponArray[i].WeaponId = i;
                state.WeaponArray[i].WeaponAmmo = Random.Range(50, 100);
            }

            //NEW: by default we don't have any weapon up, so set index to -1
            state.WeaponActiveIndex = -1;
        }

        state.AddCallback("CubeColor", ColorChanged);

        // NEW: we also setup a callback for whenever the index changes
        state.AddCallback("WeaponActiveIndex", WeaponActiveIndexChanged);
    }

    void OnGUI()
    {
        if (entity.isOwner)
        {
            GUI.color = state.CubeColor;
            GUILayout.Label("@@@");
            GUI.color = Color.white;
        }
    }

    void Update()
    {
        if (resetColorTime < Time.time)
        {
            renderer.material.color = state.CubeColor;
        }
    }

    public override void SimulateOwner()
    {
        var speed = 4f;
        var movement = Vector3.zero;

        if (Input.GetKey(KeyCode.W)) { movement.z += 1; }
        if (Input.GetKey(KeyCode.S)) { movement.z -= 1; }
        if (Input.GetKey(KeyCode.A)) { movement.x -= 1; }
        if (Input.GetKey(KeyCode.D)) { movement.x += 1; }

        // NEW: Input polling for weapon selection
        if (Input.GetKeyDown(KeyCode.Alpha1)) state.WeaponActiveIndex = 0;
        if (Input.GetKeyDown(KeyCode.Alpha2)) state.WeaponActiveIndex = 1;
        if (Input.GetKeyDown(KeyCode.Alpha3)) state.WeaponActiveIndex = 2;
        if (Input.GetKeyDown(KeyCode.Alpha0)) state.WeaponActiveIndex = -1;

        if (movement != Vector3.zero)
        {
            transform.position = transform.position + (movement.normalized * speed * BoltNetwork.frameDeltaTime);
        }

        if (Input.GetKeyDown(KeyCode.F))
        {
            var flash = FlashColorEvent.Create(entity);
            flash.FlashColor = Color.red;
            flash.Send();
        }
    }

    void ColorChanged()
    {
        GetComponent<Renderer>().material.color = state.CubeColor;
    }

    void WeaponActiveIndexChanged()
    {
        for (int i = 0; i < WeaponObjects.Length; ++i)
        {
            WeaponObjects[i].SetActive(false);
        }

        if (state.WeaponActiveIndex >= 0)
        {
            int objectId = state.WeaponArray[state.WeaponActiveIndex].WeaponId;
            WeaponObjects[objectId].SetActive(true);
        }
    }

    public override void OnEvent(FlashColorEvent evnt)
    {
        resetColorTime = Time.time + 0.25f;
        renderer.material.color = evnt.FlashColor;
    }
}

If you build your game and start a server and client, you will be able to pick a weapon using the 1, 2 and 3 keys on the keyboard, and it should look something like this.

Change Weapons Gameplay
Change Weapons Gameplay.

Currently we are not using the WeaponAmmo property for anything, but you can still see the value of it.

Continue in next chapter >>.

To Document Top