Unity Message Bus

How to make use of a message bus in Unity. This is a good solution to decouple components and logically organise how your game runs. Instead of doing all the dragging in the editor of game objects and components into scripts the message bus works a bit like a proxy for the communication between scripts. This is good (makes complex things easier to manage – ie. no noodling of dependencies everywhere – and no heavy FindObject calls – plus I get to make a funny bus pun.

Events

When using Events your Objects become Subscribers and Publishers of actions. When you subscribe (or listen) to a particular event channel you get notified when something changes. These notifications go out to every object that is listening on the bus and each object script can respond to the message in their own way.

You can make this as complex as you like but it gets harder the more complex the data is that you are passing around on the bus. This example uses just four components and our events are nice and simple:

  1. An enum that lists the Events on the Bus.
  2. The YellowBus class that handles subscribtions and publishing (by using a dictionary of events)
  3. A BigYellowBusController script that is attached to our bus game object and subscribes to all the “bus” events like starting, stopping at stops, taking on passengers, etc.
  4. A PassengerController that is attached to the Bus Rider game objects (the coloured circles) which subscribes and reacts to rider type events like calling the bus, getting on and off, and ringing the bell when you want to get off.

EventsOnTheBus

This is just a simple collection of named constants that are meaningful for our events.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AdvancedCoding.YellowBus
{
    public enum EventsOnTheBus
    {
        CALL_BUS, ALL_ABOARD, START_ENGINE, NEXT_STOP_PLEASE, STOP_ENGINE, EMERGENCY_STOP, END_O_THE_LINE
    }
}

YellowBus

This class sets up the dictionary of events matching the EventsOnTheBus to a UnityEvent call. (An even simpler example of an event call is in the docs: https://docs.unity3d.com/ScriptReference/Events.UnityEvent.html).

It exposes the Subscribe, Unsubscribe, and Publish methods.

Notice the using UnityEngine.Events directive at the top.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

namespace AdvancedCoding.YellowBus
{
    public class YellowBus
    {
        private static readonly IDictionary<EventsOnTheBus, UnityEvent>

        Events = new Dictionary<EventsOnTheBus, UnityEvent>();

        public static void Subscribe(EventsOnTheBus eventType, UnityAction listener)
        {
            UnityEvent thisEvent;

            if (Events.TryGetValue(eventType, out thisEvent))
            {
                thisEvent.AddListener(listener);
            }
            else
            {
                thisEvent = new UnityEvent();
                thisEvent.AddListener(listener);
                Events.Add(eventType, thisEvent);
            }
        }

        public static void Unsubscribe(EventsOnTheBus type, UnityAction listener)
        {
            UnityEvent thisEvent;

            if (Events.TryGetValue(type, out thisEvent))
            {
                thisEvent.RemoveListener(listener);
            }
        }

        public static void Publish(EventsOnTheBus type)
        {
            UnityEvent thisEvent;

            if (Events.TryGetValue(type, out thisEvent))
            {
                thisEvent.Invoke();
            }
        }

    }
}

BigYellowBusController

The controller on the bus is a subscriber to the events that it needs to react to and defines private methods of handling those events when they are broadcast. You can have any number of subscribers listening in to your events. In this example we only got two: The YellowBus and the Rider.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AdvancedCoding.YellowBus
{
    public class BigYellowBusController : MonoBehaviour
    {
        private string _an_event;
        private bool _engine_is_running;
        private bool _rider_waiting;
        private bool _next_stop;
        public float speed = 3;

        void OnEnable()
        {
            YellowBus.Subscribe(EventsOnTheBus.START_ENGINE, StartBus);
            YellowBus.Subscribe(EventsOnTheBus.STOP_ENGINE, StopBus);
            YellowBus.Subscribe(EventsOnTheBus.EMERGENCY_STOP, Emergency);
            YellowBus.Subscribe(EventsOnTheBus.CALL_BUS, RiderWaiting);
            YellowBus.Subscribe(EventsOnTheBus.NEXT_STOP_PLEASE, NeedToGetOff);
        }

        void OnDisable()
        {
            YellowBus.Unsubscribe(EventsOnTheBus.START_ENGINE, StartBus);
            YellowBus.Unsubscribe(EventsOnTheBus.STOP_ENGINE, StopBus);
            YellowBus.Unsubscribe(EventsOnTheBus.EMERGENCY_STOP, Emergency);
            YellowBus.Subscribe(EventsOnTheBus.CALL_BUS, RiderWaiting);
            YellowBus.Subscribe(EventsOnTheBus.NEXT_STOP_PLEASE, NeedToGetOff);
        }

        private void StartBus()
        {
            _an_event = "Started!";
            _engine_is_running = true;

        }

        private void StopBus()
        {
            _an_event = "Stopped!";
            _engine_is_running = false;

        }

        private void Emergency()
        {
            _an_event = "Emergency Stop!";
            _engine_is_running = false;
        }

        private void RiderWaiting()
        {
            _rider_waiting = true;
            _an_event = "Rider waiting...";
        }

        private void NeedToGetOff()
        {
            _next_stop = true;
            _an_event = "Passenger needs to get off...";
        }


        private void OnGUI()
        {
            GUI.color = Color.green;
            GUI.Label(new Rect(300, 60, 200, 20), "BUS EVENT is...  " + _an_event);

            if (GUILayout.Button("Start Bus Up")) YellowBus.Publish(EventsOnTheBus.START_ENGINE);
            if (GUILayout.Button("Stop Bus")) YellowBus.Publish(EventsOnTheBus.STOP_ENGINE);
        }

        public void Update()
        {
            if (_engine_is_running)
            {
                transform.position = new Vector3(transform.position.x + speed * Time.deltaTime, transform.position.y, transform.position.z);
                if (transform.position.x > 18f)
                {
                    transform.position = new Vector3(-17f, 0f, 0f);
                }

                if (_rider_waiting)
                {
                    if (transform.position.x > 0 && transform.position.x < 1)
                    {
                        YellowBus.Publish(EventsOnTheBus.STOP_ENGINE);
                        YellowBus.Publish(EventsOnTheBus.ALL_ABOARD);
                        _rider_waiting = false;
                    }
                }

                if (_next_stop)
                {
                    if (transform.position.x > 0 && transform.position.x < 1)
                    {
                        YellowBus.Publish(EventsOnTheBus.STOP_ENGINE);
                        YellowBus.Publish(EventsOnTheBus.END_O_THE_LINE);
                        _next_stop = false;
                    }
                }
            }
        }
    }
}

RiderController

The Rider Controller does pretty much the same as the Big Yellow Bus Controller but only listens to those events that relate to the riders (some of which it shares with the Big Yellow Bus Controller). Now here is where it gets interesting… look at the NEXT_STOP_PLEASE parts. Start with the OnGui() method below on line 75 where a button press will publish the NEXT_STOP_PLEASE event. In the OnEnable() method our Subscription code calls the PressTheBell() private method (line 17) that let’s our game object handle the call and produce the text message. BUT the YellowBus Controller also subscribes to this event and handles it with the NeedToGetOff() method so that it knows it has to stop and let the passenger off (line 21, 59 and 95 in the BigYellowBusController script above). AAANNDDDD responds by triggering the STOP_ENGINE event and the END_O_THE_LINE event. See how they can chain together into larger behaviours and complex interactions between different game objects without actually entangling them at the script reference level.

Also be aware that on these scripts there is a subscription in OnEnable() and an Unsubscribe in OnDisable() (so we are not holding on to the listener when the game object disappears) the thing to note here is that we are listening to our own events. For example the RiderController triggers the CALL_BUS event and both our own script and the BigYellowBusController script react to it.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AdvancedCoding.YellowBus
{
    public class RiderController : MonoBehaviour
    {

        private bool _lightMeGUIup;
        private string _message;
        private GameObject _bus;

        void OnEnable()
        {
            YellowBus.Subscribe(EventsOnTheBus.CALL_BUS, HopOnTheBus);
            YellowBus.Subscribe(EventsOnTheBus.NEXT_STOP_PLEASE, PressTheBell);
            YellowBus.Subscribe(EventsOnTheBus.ALL_ABOARD, JumpOn);
            YellowBus.Subscribe(EventsOnTheBus.END_O_THE_LINE, JumpOff);
        }

        void OnDisable()
        {
            YellowBus.Unsubscribe(EventsOnTheBus.CALL_BUS, HopOnTheBus);
            YellowBus.Unsubscribe(EventsOnTheBus.NEXT_STOP_PLEASE, PressTheBell);
            YellowBus.Unsubscribe(EventsOnTheBus.ALL_ABOARD, JumpOn);
            YellowBus.Unsubscribe(EventsOnTheBus.END_O_THE_LINE, JumpOff);

        }

        private void Start()
        {
            _bus = GameObject.Find("BigYellowBus");
        }

        private void PressTheBell()
        {
            _message = "I need to get off the bus !!";
            _lightMeGUIup = true;
        }


        private void HopOnTheBus()
        {
            _message = "I need to get on the bus please!!";
            _lightMeGUIup = true;
        }

        private void JumpOn()
        {
            _message = "Hi Driver!";
            transform.parent = _bus.transform;
            transform.position = new Vector3(0f, 0.2f, 0f);
        }

        private void JumpOff()
        {
            _message = "Thanks a lot!";
            transform.parent = null;
            transform.position = new Vector3(0f, -3f, 0f);
        }


        private void OnGUI()
        {


            if (GUI.Button(new Rect(160, 10, 100, 30), "Call Bus"))
            {
                YellowBus.Publish(EventsOnTheBus.CALL_BUS);
            }

            if (GUI.Button(new Rect(160, 50, 100, 30), "Bell Press"))
            {
                YellowBus.Publish(EventsOnTheBus.NEXT_STOP_PLEASE);
            }


            if (_lightMeGUIup)
            {
                GUI.color = Color.blue;
                GUI.Label(new Rect(300, 40, 200, 20), "..." + _message);
               StartCoroutine(DropGUI());
            }

        }

        private IEnumerator DropGUI()
        {
            yield return new WaitForSeconds(2f);
            _lightMeGUIup = false;
        }
    }
}

The other thing to note is that this message bus is like a party line – all messages are broadcast to anyone listening. All the passengers respond to the NEXT_STOP_PLEASE event as they all share the same code…but of course they could implement different controller scripts per player and all handle this event differently. It’s pretty powerful stuff.

Playtime in Bus Controller Land !

The code above was modified from an example in Game Development Patterns with Unity 2021: Explore practical game development using software design patterns and best practices in Unity and C#, 2nd Edition by David Baron – I recommend buying a copy it is the best book of this type that I have read – take yourself from a beginner to an intermediate Unity programmer. 🙂

Rust on RHEL WSL2

I’ve been doing a lot of back end investigation the past few months – trying to build something to support future projects. Most of it’s just random poking about with new technologies and different ways of doing “stuff” but then sometimes while doing this you find something that is really cool and resonates with the way your brain works. For me this month it was Rust and RHEL WSL.

I got here by a round about route of looking at golang programming and docker integration for back end processing of a game. A way to offload non-game-critical systems to other processors. Like a high score system that keeps player profiles and scores online which can be called from within the game when needed. I know there are heaps of good services out there that do this – but I like poking into stuff and having that level of control.

The other thing is – sometimes stuff just makes sense. I’ve been a long time linux user and champion but have always been locked into a Wintel desktop in the studio due to the support needs of my audio kit. (I have a Line 6 KB37 which integrates the midi keyboard and guitar effects pedals into one unit – it’s freaking awesome and I’m terrified that one day it will break and be out of support). I’d run linux as a virtual machine and used cygwin and my favourite mobaXterm as a solution to this but while poking around within the docker community I came across WSL. The Windows Subsystem for Linux.

WSL

The Windows Subsystem for Linux is a compatibility layer which runs Linux binary executables natively on Windows. WSL v2 has a real Linux kernel (though there are some storage issues if you need more info read the docs). The default linux kernel is Ubuntu which I’m OK with – one of my favourite linux distributions is the Ubuntu Studio but I’ve been a Red Hat admin for a long time so am more comfortable there but there was no RHEL or CentOS supported platform. But then I found the RHWSL project by a Japanese Developer called Yosuke Sano and I was intrigued and immediately hooked.

Basically this is how I set up WSL on my Windows 10 Workstation.

C:\Users\zulu>wsl –list –online
The following is a list of valid distributions that can be installed.
Install using ‘wsl –install -d ‘.

C:\Users\zulu>WSL –list –all
Windows Subsystem for Linux Distributions:
Ubuntu (Default)

NAME FRIENDLY NAME
Ubuntu Ubuntu
Debian Debian GNU/Linux
kali-linux Kali Linux Rolling
openSUSE-42 openSUSE Leap 42
SLES-12 SUSE Linux Enterprise Server v12
Ubuntu-16.04 Ubuntu 16.04 LTS
Ubuntu-18.04 Ubuntu 18.04 LTS
Ubuntu-20.04 Ubuntu 20.04 LTS

C:\Users\zulu>wsl –set-default-version 2
For information on key differences with WSL 2 please visit https://aka.ms/wsl2
The operation completed successfully.

C:\Users\zulu>WSL –HELP
Invalid command line option: –HELP
Copyright (c) Microsoft Corporation. All rights reserved.
Usage: wsl.exe [Argument] [Options…] [CommandLine]

I downloaded the RHWSL package from git, extracted it and run the executable to register the package with WSL and install the root file system and that was it.

This is how I set the RHWSL distribution as my default:

d:\RedHat\RHWSL>wsl -d RHWSL

Here are a bunch of useful links if you want to explore more:

https://docs.docker.com/desktop/windows/wsl/
https://docs.microsoft.com/en-us/windows/wsl/install
https://docs.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers
https://dev.to/bowmanjd/using-podman-on-windows-subsystem-for-linux-wsl-58ji

https://github.com/yosukes-dev/RHWSL
https://github.com/yosukes-dev/RHWSL/releases

If you want more info on WSL yosukes-dev has an “Awesome” resource list.

Rust

I started looking at Rust as part of a wider investigation into “modern” programming languages. I spent a few weeks looking at go (golang) and as much as it was a great multi-purpose language and super easy to start using I was a little gobsmacked at how large the binaries were (they are statically compiled) and being someone who is always on tiny machines I kept getting a niggling feeling that a whole system of go programs would be a big chunk of disk on a small device doing work that that might be easier done slower with a simple shell script (I exaggerate!). Anyway in a lot of the stuff I read golang and rust were comparable. I will come back to go as the community and contributions seemed most excellent.

Plus Rust made sense to me in places where Go didn’t. I really don’t have a logical excuse here or a well reasoned argument. Sometimes you just like a language cause it “feels” right. I had the same thing with Ruby. Anyway this is how I got started with Rust on the RHWSL…

https://www.rust-lang.org/learn/get-started
If you’re a Windows Subsystem for Linux user run the following in your terminal, then follow the on-screen instructions to install Rust.

curl –proto ‘=https’ –tlsv1.2 -sSf https://sh.rustup.rs | sh

I started following the hello world tutorial to set up the system and it wasn’t all plain sailing. For one I probably should have done the install as a normal user but after mucking around with environment variables and permissions into the root user directories it was just easier to do the work as root. (I know rolling over in my grave). So it wasn’t all plain sailing – and once I got to the compile stage there were a few basic compiling tools that the RHWSL needed. This is how it went:

https://doc.rust-lang.org/cargo/getting-started/first-steps.html

[root@Venom RHWSL]# mkdir rustProgramming
[root@Venom RHWSL]# cd rustProgramming/
[root@Venom rustProgramming]# cargo new hello_world
Created binary (application) hello_world package
[root@Venom rustProgramming]#
[root@Venom rustProgramming]# ls -ltr
total 0
drwxrwxrwx 1 root root 4096 Feb 8 18:27 hello_world
[root@Venom rustProgramming]#
[root@Venom rustProgramming]# cd hello_world/
[root@Venom hello_world]# ls -ltr
total 0
-rwxrwxrwx 1 root root 180 Feb 8 18:27 Cargo.toml
drwxrwxrwx 1 root root 4096 Feb 8 18:27 src

[root@Venom hello_world]# cat Cargo.toml
[package]
name = “hello_world”
version = “0.1.0”
edition = “2021”
See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[root@Venom hello_world]# cat src/main.rs
fn main() {
println!(“Hello, world!”);
}

[root@Venom hello_world]# cargo build
Compiling hello_world v0.1.0 (/mnt/d/RedHat/RHWSL/rustProgramming/hello_world)
error: linker cc not found
|
= note: No such file or directory (os error 2)
error: could not compile hello_world due to previous error

Bingo – first problem – no compiler. I installed make and gcc – not really sure if I needed make but figured it would be a nice to have anyway.

[root@Venom hello_world]# which make
/usr/bin/which: no make in (/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/mnt/c/Program Files (x86)/ ……….lots more here

[root@Venom hello_world]# which cc
/usr/bin/which: no cc in (/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Program Files (x86)/Common Files/Oracle/Java/javapath:/mnt/c/Program Files (x86)/ ……….lots more here

[root@Venom hello_world]# yum install make
…Dependencies resolved.
Package Architecture Version Repository Size
Installing:
make x86_64 1:4.2.1-10.el8 ubi-8-baseos 498 k
…. more in here you don’t need to see …
Installed:
make-1:4.2.1-10.el8.x86_64
Complete!

[root@Venom hello_world]# which make
/usr/bin/make

[root@Venom hello_world]# yum install gcc
…Dependencies resolved
…. more in here you don’t need to see …
Installing dependencies:
binutils x86_64 2.30-108.el8_5.1 ubi-8-baseos 5.8 M
cpp x86_64 8.5.0-4.el8_5 ubi-8-appstream 10 M
glibc-devel x86_64 2.28-164.el8 ubi-8-baseos 1.0 M
glibc-headers x86_64 2.28-164.el8 ubi-8-baseos 480 k
…Transaction Summary
Install 14 Packages
Total download size: 51 M
Installed size: 123 M
Installed:
Complete!

[root@Venom hello_world]# which cc
/usr/bin/cc

[root@Venom hello_world]# cargo build
Compiling hello_world v0.1.0 (/mnt/d/RedHat/RHWSL/rustProgramming/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 2.09s

[root@Venom hello_world]# ls -ltr
total 0
-rwxrwxrwx 1 root root 180 Feb 8 18:27 Cargo.toml
drwxrwxrwx 1 root root 4096 Feb 8 18:27 src
-rwxrwxrwx 1 root root 155 Feb 8 18:28 Cargo.lock
drwxrwxrwx 1 root root 4096 Feb 8 18:28 target

You can run the executable like this:

[root@Venom hello_world]# ./target/debug/hello_world
Hello, world!

Or just run from command:

[root@Venom hello_world]# cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running target/debug/hello_world
Hello, world!

Yay !