Unnatural Disaster

Global Game Jam 2014. It’s late Saturday night, on the eve of our home stretch. My team was well ahead of schedule. We’d gone with an unusual project, a card game, so we’d been playtesting in no time, working well together all weekend and making great progress. Many hands make light work, and with ten people on our team, work was plenty light.

But with everything going so smoothly, it was almost as if I was missing out on the real jam experience. Looking around at the other teams struggling to finish, I almost felt… guilty. So, I decided to up the challenge for myself a bit. As the clock rolled over midnight – less than a day to go – I popped open Unity and started working on a second game, this time on my own.

I called the project Unnatural Disaster.

It’s an idea that I’d had in the back of my mind for a long time. Not even an idea really, just the faintest whiff of a concept: a tablet game with a little metropolis I could annihilate with various natural disasters. I brought it up at the team pitch, and we (fairly) decided to go in a different direction. But now, with one successful project all but in the bag, what did I have to lose?

So I sketched out a rough outline of the experience. There’d be the city, of course, and residents going about their business. There were plenty of possible objectives – destroy certain buildings or individuals, limit or encourage collateral damage, or just pulverize everything. The most practical objective turned out to be achieving a minimum amount of destruction, but staying under some maximum (we need to humble our little denizens, not wipe them out). This was easy to implement, and encouraged experimenting with multiple disasters to zero in on a sweet spot and discouraging spamming the most potent abilities.

No more time to plan. Maybe enough time to build, if I’m lucky.

The City

The first challenge was to make a reasonably good-looking approximation of a city. My initial thought was just to plunk down a bunch of boxes of different shapes and sizes, but fiddling with that would have burned too much time. Instead, I decided to go procedural. By parceling the city into a grid, I could tackle the smaller problem of making a reasonably good-looking approximation of a city block and extrapolate from there.

WARNING: This code was made at a frantic pace and with little sleep, and it shows. It’s a demonstration of triage and practicality, not of best practices.

using UnityEngine;

/// <summary>
/// BlockBuilder class.
/// </summary>
public class BlockBuilder : MonoBehaviour
{
	/// <summary>
	/// Building prefab to copy.
	/// </summary>
	public GameObject buildingPrefab;

	public float bigBuilding = 2f;
	public float smallBuilding = 0.9f;
	public float offset = 0.55f;
	public float skyscraperRate = 0.05f;

    /// <summary>
    /// Initialize script state.
    /// </summary>
    internal void Start()
    {
		var buildingCount = Random.Range(1, 5);

		switch (buildingCount)
		{
			case 1:
				this.InstantiateBuilding(Locale.Center);

				break;
			case 2:
				if (Random.value < 0.5f)
				{
					this.InstantiateBuilding(Locale.East);
					this.InstantiateBuilding(Locale.West);
				} 
				else
				{
					this.InstantiateBuilding(Locale.North);
					this.InstantiateBuilding(Locale.South);
				}

				break;
			case 3:
				switch (Random.Range(0, 4))
				{
					case 0:
						this.InstantiateBuilding(Locale.SouthEast);
						this.InstantiateBuilding(Locale.SouthWest);
						this.InstantiateBuilding(Locale.North);
						break;
					case 1:
						this.InstantiateBuilding(Locale.NorthWest);
						this.InstantiateBuilding(Locale.SouthWest);
						this.InstantiateBuilding(Locale.East);
						break;
					case 2:
						this.InstantiateBuilding(Locale.NorthEast);
						this.InstantiateBuilding(Locale.NorthWest);
						this.InstantiateBuilding(Locale.South);
						break;
					case 3:
						this.InstantiateBuilding(Locale.NorthEast);
						this.InstantiateBuilding(Locale.SouthEast);
						this.InstantiateBuilding(Locale.West);
						break;
				}

				break;
			case 4:
				this.InstantiateBuilding(Locale.NorthEast);
				this.InstantiateBuilding(Locale.SouthEast);
				this.InstantiateBuilding(Locale.SouthWest);
				this.InstantiateBuilding(Locale.NorthWest);

				break;
		}
    }

	private void InstantiateBuilding(Locale locale)
	{
		var isNotSkyscraper = Random.value > this.skyscraperRate;

		// height between 0.5 and 2.5, or 4.0 and 5.0
		var height = Constants.floorHeight * Random.Range(isNotSkyscraper ? 1 : 8, isNotSkyscraper ? 6 : 11);

		var building = (GameObject)Instantiate(this.buildingPrefab);
		building.transform.parent = this.transform;
		building.name = string.Format("Building ({0})", locale);

		var size = new Vector2(this.bigBuilding, this.bigBuilding);
		var location = Vector2.zero;

		switch (locale)
		{
			case Locale.North:
				size = new Vector2(this.bigBuilding, this.smallBuilding);
				location = new Vector2(0f, this.offset);
				break;
			case Locale.East:
				size = new Vector2(this.smallBuilding, this.bigBuilding);
				location = new Vector2(this.offset, 0f);
				break;
			case Locale.South:
				size = new Vector2(this.bigBuilding, this.smallBuilding);
				location = new Vector2(0f, -this.offset);
				break;
			case Locale.West:
				size = new Vector2(this.smallBuilding, this.bigBuilding);
				location = new Vector2(-this.offset, 0f);
				break;
			case Locale.NorthEast:
				size = new Vector2(this.smallBuilding, this.smallBuilding);
				location = new Vector2(this.offset, this.offset);
				break;
			case Locale.SouthEast:
				size = new Vector2(this.smallBuilding, this.smallBuilding);
				location = new Vector2(this.offset, -this.offset);
				break;
			case Locale.SouthWest:
				size = new Vector2(this.smallBuilding, this.smallBuilding);
				location = new Vector2(-this.offset, -this.offset);
				break;
			case Locale.NorthWest:
				size = new Vector2(this.smallBuilding, this.smallBuilding);
				location = new Vector2(-this.offset, this.offset);
				break;
		}

		building.transform.localPosition = new Vector3(location.x, (height * Constants.floorHeight) + 0f, location.y);
		building.transform.localScale = new Vector3(size.x, height, size.y);
	}
	
    /// <summary>
    /// Locale enum.
    /// </summary>
	private enum Locale
	{
		Center,
		North,
		NorthEast,
		East,
		SouthEast,
		South,
		SouthWest,
		West,
		NorthWest,
	}
}

With 4 different block types and 8 possible configurations, there was a healthy mix of different building shapes. For height, I made a distinction between regular buildings (1-6 stories) and skyscrapers (8-11 stories). Filled in, this resulted in pretty decent skyline:

unnatural_disaster_skyline

Now that I had my city, it was time to architect its destruction.

The Disasters

I had lofty ambitions for my disasters. Imagine shaking a tablet back and forth to slosh a sea around, inundating the town with a tsunami! But I needed realistic and achievable, and a fluid simulation was anything but. Instead, I pursued four simple disasters that provided as diverse and strategic a collection of mechanics I could hope for:

Earthquake: All the disasters follow the same pattern – click the disaster icon in the bottom-left to select, then click somewhere in the city to trigger. The earthquake adds extra depth by allowing the player to vary the strength; keep clicking, and its power rises from 1 (very little effect) to 5 (level half the city). In terms of implementation, it simply shakes the ground plane for a short amount of time.

Tornado: Probably the most complex disaster. Implemented as a cylindrical trigger collider, it responds to collisions with rigidbodies by adding a small torque to them. Built up over time, this will spin and collapse the buildings. Like earthquake, it has a configurable effect: click to place, then click anywhere else to set a destination – the tornado will slowly work its way in that direction, causing much more destruction than a stationary twister.

Meteor: Just a rigidbody dropped out of the sky. It’s pretty much an instant kill, but its limited area of effect means it’s best used for precisely targeting very large buildings.

Lightning: Lightning has no effect in the game. I ran out of time before finishing it! It was originally meant to target individuals in a crowd, if crowds hadn’t been cut. The backup plan was for it to start a building on fire, but the effect wasn’t differentiated enough from meteor to be worth investing in. Instead, it’s just a cool-looking way to waste a turn.

With my cadre of disasters, I was getting close to having a game. Players had three chances to use any combination of the above to destroy the right amount of buildings (and cause the right amount of casualties) to win. Wait, destruction? Casualties? These are still just boxes! How do I get from “box” to “building”?

The Buildings

using UnityEngine;

/// <summary>
/// Building class.
/// </summary>
[RequireComponent(typeof(Renderer))]
public class Building : MonoBehaviour
{
	private const int PeoplePerUnitFloorspace = 50;

	/// <summary>
	/// Population manager, set by parent.
	/// </summary>
	//public PopulationManager populationManager;

	/// <summary>
	/// Population of self.
	/// </summary>
	public int population;

	/// <summary>
	/// Whether the building is intact or destroyed.
	/// </summary>
	public bool isIntact = true;

	/// <summary>
	/// Position at beginning of simulation.
	/// </summary>
	private Vector2 homePosition;

    /// <summary>
    /// Initialize script state.
    /// </summary>
    internal void Start()
    {
		var floorSize = this.transform.localScale.x * this.transform.localScale.z;
		var floors = (int)(this.transform.localScale.y / Constants.floorHeight);
		var floorSpace = floorSize * floors;
		this.population = (int)(floorSpace * PeoplePerUnitFloorspace);
        //Debug.Log("with a floor size of " + floorSize + " and " + floors + " floors, floorspace = " + floorSpace +
        //          " and population = " + this.population);

		//this.populationManager.population += this.population;
        GlobalEventAggregator.GetEvent<BuildingCreated>().Publish(this.population);

		this.homePosition = new Vector2(this.transform.localPosition.x, this.transform.localPosition.z);
    }

	/// <summary>
	/// Update script, called on a fixed interval.
	/// </summary>
	internal void FixedUpdate()
	{
		if (!this.isIntact)
		{
            if (this.transform.position.y < -10f)
            {
                Destroy(this.gameObject);
            }

			return;
		}

		var distance = Vector2.Distance(this.homePosition, new Vector2(this.transform.localPosition.x, this.transform.localPosition.z));
		if (distance > 0.02f)
		{
			Debug.Log("high building movement " + distance + ", collapse");
			this.Collapse();
		}

		var rotation = Quaternion.Angle(Quaternion.identity, this.transform.localRotation);
		if (rotation > 2f)
		{
			Debug.Log("high building rotation " + rotation + ", collapse");
			this.Collapse();
		}
	}

	/// <summary>
	/// Called when this collider/rigidbody has begun touching another rigidbody/collider.
	/// </summary>
	/// <param name="collision">Collision details.</param>
	internal void OnCollisionEnter(Collision collision)
	{
		if (!this.isIntact)
		{
			return;
		}

		if (collision.relativeVelocity.magnitude > 1f)
		{
			Debug.Log("high building forces " + collision.relativeVelocity.magnitude + ", collapse");
			this.Collapse();
		}
	}

	/// <summary>
	/// Destroy this building.
	/// </summary>
	private void Collapse()
	{
		this.isIntact = false;

	    //this.populationManager.casualties += this.population;
        GlobalEventAggregator.GetEvent<BuildingCollapsed>().Publish(this.population);

        this.GetComponent<Renderer>().material.color = Color.red;
        
        Destroy(this.GetComponent<Collider>());

	    var particleEffectManager = FindObjectOfType<ParticleEffectManager>();
        if (particleEffectManager)
        {
            particleEffectManager.InstantiateParticles(particleEffectManager.dustPrefab, new Vector3(this.transform.position.x, 0f, this.transform.position.z));
        }
	}
}

Buildings have two responsibilities: manage their share of the population, and collapse. For the first task, buildings determine how much floorspace they have, which is then used to calculate how many people are inside. This task could have gone to the block builder, but since buildings need to know their own population in order to transmit how many casualties occur when they collapse, it made sense to put it here.

Next was to detect and respond to damage. I didn’t bother with health or anything; a building simply collapses if it moves too far (or rotates too much) from its initial position. It worked, and resulted in the right amount of destruction for each disaster. The actual collapse effect was surprisingly easy: I just turned off the building’s collider and it fell through the ground in a natural-looking way. A little bit of smoke and dust sealed the deal.

The End

As I mentioned in my game jam advice, closing the loop (menu to game to menu) is critical. Players may be willing to forgive rough-looking graphics or incomplete features, but if the only way to restart the game is to refresh the webpage or relaunch the executable, few will realize or bother. So with the deadline looming, I rushed out a simple title screen, slapped a tornado behind it, and hit “export”… just under the wire!

So yes, it’s papier-mâché and wire masquerading as a game. But there’s just enough polish that it feels like it could be a game, and for something cobbled together inside of a day, that’s a major win.

I’d also like to point out that, while I developed the game alone, I couldn’t have made Unnatural Disaster without the help of some indispensable co-jammers. Allyce Rusnak provided illustrations for the disaster icons while helping wrap up Natural Kingdom. And Joseph Clark’s incredible music and sound design were instrumental in making the game something special.

Before this experience, I thought I had a pretty good handle on prioritization and practical solutions. But the crucible of the game jam, the terrifying and joyful freedom to abandon all pretense or concern for the future and just get shit done, brought a clear focus to my personal development ethos that still defines my work today.

Bookmark the permalink.

Comments are closed