Reactive Invaders

Let’s use Reactive Extension to build something a bit more fun: a simple version of Space Invaders. We have twenty aliens distributed inside a 5×4 grid. The game object acting as the container for the aliens move across the screen back and forth and then slowly downwards.

At the bottom of the screen we have our gun. It moves across the screen non-stop, back and forth. If the player double clicks the mouse we spawn a bullet. Bullets take out the aliens, and we update a counter of how many aliens are still alive.

We need prefabs for the gun, aliens and bullets. Plus a UI text field to display the aliens count, and a reference to the aliens container which is already in the scene and positioned at the top left corner of the screen.

1
2
3
4
5
6
7
8
9
10
11
12
public GameObject gunGO;
public GameObject alienGO;
public GameObject gunBulletGO;
public Text aliensCount;
public GameObject aliensContainer;
 
private float alienSpeed = 0.04f;
private float gunXSpeed = 0.02f;
private GameObject gun;
private int columns = 5;
private int rows = 4;
private float spaceBetweenAliens = 0.5f;

Our first UniRX object. A reactive list of aliens. This will turn a number of common collection operations into signals. So we can subscribe to when items are added or removed from the list for example.

1
private ReactiveCollection<GameObject> aliens;

The entire game is created inside the Start method.

I start with the Aliens. I create the reactive collection and subscribe to the signals I’ll need.

So whenever an alien is added to the collection, I place it inside the grid and inside the alien container. And then I subscribe to the OnEnter trigger collision signal in each Alien and destroy the collider gameobject (the object colliding with the alien) and then remove the alien from the list.

Then I subscribe to whenever an alien is removed from the list, which destroys the alien’s gameObject.

Finally I subscribe to whenever the count in the collection changes and I update the text field with the alien count.

After all that’s done, I spawn the aliens.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//CREATE ALIENS
aliens = new ReactiveCollection<GameObject>();
 
aliens.ObserveAdd()
.Subscribe(x => {
	x.Value.transform.SetParent(aliensContainer.transform, false);
	x.Value.transform.localPosition = new Vector2 (
		(x.Index % columns) * spaceBetweenAliens,
		Mathf.FloorToInt(x.Index/columns) * -spaceBetweenAliens
	);
 
	x.Value.OnTriggerEnter2DAsObservable ()
	.Where (g => g.tag == "GunBullet" )
	.Subscribe (g => {
			aliens.RemoveAt(x.Index);
			Destroy(g.gameObject);
		}
	);
});
 
aliens.ObserveRemove()
.Subscribe(x => {
	GameObject.Destroy(x.Value);
});
 
aliens.ObserveCountChanged ()
.Subscribe (x => aliensCount.text = "Aliens: " + x);
 
var total = columns * rows;
var i = 0;
while (i < total) {
	var alien = Instantiate (alienGO) as GameObject;
	aliens.Add (alien);
	i++;
}

Next, I deal with the gun.

All I need is to subscribe to the collision signal. The aliens don’t shoot, I wanted to keep this really simple, but it would not be too hard to implement that and I leave it as an exercise!

1
2
3
4
5
6
7
8
 
//CREATE GUN
gun = Instantiate (gunGO) as GameObject;
gun.transform.position = new Vector2 (0, -4.5f);
 
gun.OnTriggerEnter2DAsObservable ()
.Where (x => x.tag == "Alien" )
.Subscribe (x => Destroy(gun));

And then I create a game loop subscription.

I sampled the frame rate so that the logic runs every 2 frames. Just so I could show one more thing to use in compositions.

Notice that I don’t susbscribe immediately to the game loop. The observable does not exist yet.

Then I create two new signals by suscribing to the same game loop. I could create new compositions for each one. I could combine them, I could do whatever the hell I wanted.

1
2
3
4
5
 
//MAIN LOOP
var gameUpdate = Observable.EveryUpdate ()
.TakeUntilDestroy (gun)
.SampleFrame (2);

First I move the gun.

1
2
3
4
5
6
 
var moveGun = gameUpdate
.Subscribe (_ =>  {
	gun.transform.Translate (new Vector3(gunXSpeed, 0, 0));
	if (gun.transform.position.x > 2.5f || gun.transform.position.x < -2.5f) gunXSpeed *= -1;
});

Then I move the alien’s container.

1
2
3
4
5
6
7
8
9
 
var moveAliens = gameUpdate
.Subscribe (_ =>  {
	aliensContainer.transform.Translate(new Vector3(alienSpeed, 0, 0));
	if (aliensContainer.transform.position.x > 0.5f || aliensContainer.transform.position.x < -2.5f) {
		aliensContainer.transform.Translate(new Vector3(0, -0.1f, 0));
		alienSpeed *= -1;
	}
});

And for kicks I used the same double click example from UniRX package and used that to spawn the bullets. Showing both the power of UniRX and Copying and Pasting! 🙂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
var userInput = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0));
 
userInput.Buffer(userInput.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Subscribe(xs => {
		//spawn bullet on double click
		var b = Instantiate (gunBulletGO) as GameObject;
		b.transform.position = new Vector2( gun.transform.position.x, -4.5f);
 
		b.UpdateAsObservable()
		.Subscribe (_ => b.transform.Translate (new Vector3(0, 0.1f, 0) ));
	}
);

Like the Breathe app example, you should know that I could have accomplished the same tasks in a gazillion different ways.

I could have used a Reactive Integer Property for the aliens count and subscribe it straight to the text field.

But more than that, I could change the game very easily. I could merge signals so that aliens only move when I shoot, in fact creating a turn base, roguelike game could be super simple.

Here’s the entire code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using UniRx;
using UniRx.Triggers;
 
public class ReactiveInvaders : MonoBehaviour {
	public GameObject gunGO;
	public GameObject alienGO;
	public GameObject gunBulletGO;
	public Text aliensCount;
	public GameObject aliensContainer;
 
	private float alienSpeed = 0.04f;
	private float gunXSpeed = 0.02f;
	private GameObject gun;
 
	private ReactiveCollection<GameObject> aliens;
 
	private int columns = 5;
	private int rows = 4;
	private float spaceBetweenAliens = 0.5f;
 
 
	void Start () {
 
		//CREATE ALIENS
		aliens = new ReactiveCollection<GameObject>();
 
		aliens.ObserveAdd()
		.Subscribe(x => {
			x.Value.transform.SetParent(aliensContainer.transform, false);
			x.Value.transform.localPosition = new Vector2 (
				(x.Index % columns) * spaceBetweenAliens,
				Mathf.FloorToInt(x.Index/columns) * -spaceBetweenAliens
			);
 
			x.Value.OnTriggerEnter2DAsObservable ()
			.Where (g => g.tag == "GunBullet" )
			.Subscribe (g => {
					aliens.RemoveAt(x.Index);
					Destroy(g.gameObject);
				}
			);
		});
 
		aliens.ObserveRemove()
		.Subscribe(x => {
			GameObject.Destroy(x.Value);
		});
 
		aliens.ObserveCountChanged ()
		.Subscribe (x => aliensCount.text = "Aliens: " + x);
 
		var total = columns * rows;
		var i = 0;
		while (i < total) {
			var alien = Instantiate (alienGO) as GameObject;
			aliens.Add (alien);
			i++;
		}
 
 
		//CREATE GUN
		gun = Instantiate (gunGO) as GameObject;
		gun.transform.position = new Vector2 (0, -4.5f);
 
		gun.OnTriggerEnter2DAsObservable ()
		.Where (x => x.tag == "Alien" )
		.Subscribe (x => Destroy(gun));
 
 
		//MAIN LOOP
		var gameUpdate = Observable.EveryUpdate ()
		.TakeUntilDestroy (gun)
		.SampleFrame (2);
 
 
		var moveGun = gameUpdate
		.Subscribe (_ =>  {
			gun.transform.Translate (new Vector3(gunXSpeed, 0, 0));
			if (gun.transform.position.x > 2.5f || gun.transform.position.x < -2.5f) gunXSpeed *= -1;
		});
 
		var moveAliens = gameUpdate
		.Subscribe (_ =>  {
			aliensContainer.transform.Translate(new Vector3(alienSpeed, 0, 0));
			if (aliensContainer.transform.position.x > 0.5f || aliensContainer.transform.position.x < -2.5f) {
				aliensContainer.transform.Translate(new Vector3(0, -0.1f, 0));
				alienSpeed *= -1;
			}
		});	
 
 
		var userInput = Observable.EveryUpdate()
		.Where(_ => Input.GetMouseButtonDown(0));
 
		userInput.Buffer(userInput.Throttle(TimeSpan.FromMilliseconds(250)))
		.Where(xs => xs.Count >= 2)
		.Subscribe(xs => {
				//spawn bullet on double click
				var b = Instantiate (gunBulletGO) as GameObject;
				b.transform.position = new Vector2( gun.transform.position.x, -4.5f);
 
				b.UpdateAsObservable()
				.Subscribe (_ => b.transform.Translate (new Vector3(0, 0.1f, 0) ));
			}
		);
	}
}