Go back

Bridge Control

A game about keeping boats from crashing into your bridges!




About this project

Engine Unity/C#
Platform(s) Android, itch.io, UWP
In Bridge Control it is your job to make sure the road and water traffic keep moving!

But, that's not as easy as it might sound. The boats just don't seem to care whether the bridges are open or closed. They'll just keep going! To avoid crashes you're going to have to open and close them at the right time. Are you up to the task?

Bridge Control originally started as a student project. Together with Hannes Vernooij and Tim Schipper I did a shortened version of our game education. To complete this we needed to make a game, which then became Bridge Control.

It turned out our plans for this game were way too big to finish for this school project. So, we just kept going after the project finished. We're now finally nearing completion and will be releasing this game for Android soon. After that we will look into different platforms.

My role in this project was mostly the programming and game design. I created the vehicle AI and XML level formats, level sharing and many more things. I also tried a lot of new things for this project. For example, I added video advertisements and extensive game analytics. I also created a level tester AI, that can play levels on it's own at higher game speeds which saves us a lot of time on level development.

An older prototype is available on the Google Play Store. The link can be found below.

Downloads and links

LevelTester.cs

A script that can automatically, at a higher timescale, test a level and determine down time and active time.

                            
#if UNITY_EDITOR
using LevelEditor;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LevelTesterAI : MonoBehaviour
{
	private const string LevelTesterAIKey = "Level Tester AI";

	private Coroutine _playLevelLoop;
	private LevelTesterAction _action;

	private float _timeSpentWaiting = 0;
	private float _playTime = 0;
	private bool _active;

	private void Start()
	{
		DeveloperMenu.Instance.AddDeveloperMenu(LevelTesterAIKey, DrawWindow);
	}

	private void OnDestroy()
	{
		DeveloperMenu.Instance.RemoveDeveloperMenu(LevelTesterAIKey);
	}

	private void DrawWindow()
	{
		if(GUILayout.Button("Let AI play level"))
		{
			_playLevelLoop = StartCoroutine(PlayLevelLoop());
			_active = true;
		}
	}

	private IEnumerator PlayLevelLoop()
	{
		_action = new WaitForActivity();
		_action.StartAction(this);
		yield return null;

		while(true)
		{
			if(Game.instance.gameOver)
			{
				StopCoroutine(_playLevelLoop);
				_playLevelLoop = null;
			}

			if(_action.GetType() == typeof(WaitForActivity)) _timeSpentWaiting += Time.deltaTime;
			_playTime += Time.deltaTime;

			_action.DoAction();

			if(_action.IsFinished)
			{
				Debug.Log("Action is finished");
				SwitchAction(new WaitForActivity());
			}
			else
			{
				Debug.Log("Waiting for action to finish");
			}

			yield return null;
		}
	}

	public void SwitchAction(LevelTesterAction action)
	{
		if(_playLevelLoop != null)
		{
			_action = action;
			_action.StartAction(this);
		}
		else
		{
			Debug.LogError("Play level loop is not running!");
		}
	}

	private void OnGUI()
	{
		if(_active)
		{
			if(Game.instance.gameOver)
			{
				GUI.Box(new Rect(Screen.width / 2 - 200, Screen.height / 2 - 50, 400, 100), string.Format("Time spent playing: {0}\nTime spent waiting: {1}", _playTime, _timeSpentWaiting));
			}

			if(_playLevelLoop == null || _action == null) return;

			GUI.Box(new Rect(Screen.width - 500, Screen.height - 50, 500, 50), string.Format("Current action: {0}\nTime spent playing: {1}\nTime spent waiting: {2}", _action, _playTime, _timeSpentWaiting));
		}
	}

	public interface LevelTesterAction
	{
		bool IsFinished { get; }
		void StartAction(LevelTesterAI levelTester);
		void DoAction();
	}

	private class WaitForActivity : LevelTesterAction
	{
		public const int CheckRange = 12;
		public const int OpenRange = 12;

		private LevelTesterAI _levelTester;

		private Dictionary _actionNeededBridges = new Dictionary();

		public bool IsFinished
		{
			get; private set;
		}

		public void StartAction(LevelTesterAI levelTester)
		{
			IsFinished = false;
			_levelTester = levelTester;
			Debug.Log("Starting wait action");
		}

		public void DoAction()
		{
			CheckVehicleType(VehicleType.Car, Game.instance.CarGrid, "Car", 2, 5);
			CheckVehicleType(VehicleType.Train, Game.instance.TrainGrid, "Train", 2);
			CheckVehicleType(VehicleType.Boat, Game.instance.BoatGrid, "Boat", 6, 100);

			if(_actionNeededBridges.Count >= 0)
			{
				DoMostImportantAction();
			}

			_actionNeededBridges.Clear();
		}

		private void DoMostImportantAction()
		{
			int highestPriority = -1;
			ActionNeededBridge mostImportantBridge = null;
			foreach(KeyValuePair kVP in _actionNeededBridges)
			{
				if(kVP.Value.Priority > highestPriority && kVP.Value.IsPossibleRightNow())
				{
					mostImportantBridge = kVP.Value;
				}
			}

			if(mostImportantBridge != null)
			{
				Vector3 pos = new Vector3(mostImportantBridge.Bridge.transform.position.x, mostImportantBridge.Bridge.transform.position.y, -10);

				VehicleType otherType = mostImportantBridge.Bridge.OpenVehicleType == mostImportantBridge.GridType ? mostImportantBridge.Bridge.ClosedVehicleType : mostImportantBridge.Bridge.OpenVehicleType;
				Action action;
				if(mostImportantBridge.Bridge is BreakableBridge)
				{
					action = () => _levelTester.SwitchAction(new BreakableBridgeFixAction((BreakableBridge)mostImportantBridge.Bridge));
				}
				else
				{
					action = () => _levelTester.SwitchAction(new SwitchBridgeAction(mostImportantBridge.Bridge, otherType));
				}
				_levelTester.SwitchAction(new MoveAction(pos, action));
			}
		}

		private void CheckVehicleType(VehicleType type, GameObject[][] grid, string tag, int lookAheadRange, int basePriority = 0)
		{
			for(int x = 0, xLength = grid.Length; x < xLength; x++)
			{
				for(int y = 0, yLength = grid[0].Length; y < yLength; y++)
				{
					//Nothing here, moving on
					if(grid[x][y] == null)
					{
						continue;
					}

					if(grid[x][y].tag != tag)
					{
						continue;
					}

					//Found a vehicle!
					AI ai = grid[x][y].GetComponent();

					//Let's check if there's a bridge in the way
					for(int i = 0; i < lookAheadRange; i++)
					{
						int position = ai.Path.pathPosition + i;

						//The position we wanted to look up is higher than the path length
						//which means the vehicle is almost at the end.
						if(ai.Path.path.Length <= position)
						{
							continue;
						}

						Vector2 pathPosition = ai.Path.path[position];

						if(i > DistanceOpenRange(new Vector2(Camera.main.transform.position.x, Camera.main.transform.position.y), pathPosition))
						{
							continue;
						}

						IntVector2 gridPosition = GameManager.Instance.Get().Get().WorldToGridPos(pathPosition);
						GameObject gO = grid[gridPosition.Y][gridPosition.X];

						if(gO == null)
						{
							continue;
						}

						//Looks like there's a bridge in the way!
						if(gO.tag == "Bridge")
						{
							Bridge bridge = gO.GetComponent();

							if(bridge is NormalBridge && ((NormalBridge)bridge).IsTransitioning)
							{
								continue;
							}

							if(_actionNeededBridges.ContainsKey(bridge))
							{
								ActionNeededBridge actionBridge = _actionNeededBridges[bridge];
								actionBridge.Priority += basePriority + lookAheadRange - i;
								actionBridge.VehiclesWaiting++;
								_actionNeededBridges[bridge] = actionBridge;
							}
							else
							{
								_actionNeededBridges.Add(bridge, new ActionNeededBridge(type, basePriority + i, bridge));
							}
						}
					}
				}
			}
		}

		private float DistanceOpenRange(Vector2 pointA, Vector2 pointB)
		{
			float dist = Vector2.Distance(pointA, pointB);
			float distOverTime = dist / 10;
			return 2 + distOverTime;
		}

		private class ActionNeededBridge
		{
			private readonly VehicleType[] _gameOverVehicleTypes = { VehicleType.Boat };

			public VehicleType GridType;
			public int Priority;
			public int VehiclesWaiting;
			public Bridge Bridge;

			public ActionNeededBridge(VehicleType gridType, int priority, Bridge bridge)
			{
				GridType = gridType;
				Priority = priority;
				Bridge = bridge;
				VehiclesWaiting = 1;
			}

			public bool IsPossibleRightNow()
			{
				if(Bridge is BreakableBridge)
				{
					return true;
				}

				VehicleType otherType;
				if(Bridge.OpenVehicleType == GridType)
				{
					otherType = Bridge.ClosedVehicleType;
				}
				else
				{
					otherType = Bridge.OpenVehicleType;
				}

				bool gameOverType = false;
				for(int i = 0, length = _gameOverVehicleTypes.Length; i < length; i++)
				{
					if(_gameOverVehicleTypes[i] == otherType)
					{
						gameOverType = true;
					}
				}

				if(!gameOverType)
				{
					return true;
				}

				Vector2 bridgePosition = new Vector2(Bridge.transform.position.x, Bridge.transform.position.y);
				GameObject[][] grid = GetVehicleTypeGrid(otherType);
				for(int x = 0, xLength = grid.Length; x < xLength; x++)
				{
					for(int y = 0, yLength = grid[0].Length; y < yLength; y++)
					{
						GameObject gO = grid[x][y];
						//Makes the assumption VehicleType is the same as tag
						if(gO == null || gO.tag != otherType.ToString())
						{
							continue;
						}

						AI ai = gO.GetComponent();
						for(int i = 0; i < 10; i++)
						{
							int position = ai.Path.pathPosition + i;

							if(ai.Path.path.Length <= position || ai.Path.path[position] != bridgePosition)
							{
								continue;
							}

							float timeLeft = ai.Speed * Mathf.Max(0, i - 2);
							//Makes the assumption vehicle speed is 3. NEEDS TO BE CHANGED.
							float canPerformIn = (VehiclesWaiting * (Constants.tileCellSize / 3f)) / 5f;
							if(timeLeft - 1 < canPerformIn)
							{
								return false;
							}
						}
					}
				}

				return true;
			}

			private GameObject[][] GetVehicleTypeGrid(VehicleType type)
			{
				switch(type)
				{
					case VehicleType.Car:
						return Game.instance.CarGrid;
					case VehicleType.Train:
						return Game.instance.TrainGrid;
					case VehicleType.Boat:
					default:
						return Game.instance.BoatGrid;
				}
			}
		}
	}

	private class MoveAction : LevelTesterAction
	{
		public const float Speed = 15;

		private Vector3 _position;
		private Action _onFinished;
		private bool _complete = false;

		Transform _camera;

		public MoveAction(Vector3 position, Action onFinished)
		{
			_position = position;
			_onFinished = onFinished;
		}

		public bool IsFinished
		{
			get; private set;
		}

		public void StartAction(LevelTesterAI levelTester)
		{
			IsFinished = false;
			_camera = Camera.main.transform;
		}

		public void DoAction()
		{
			if(_complete)
			{
				IsFinished = true;
				return;
			}

			_camera.position = Vector3.MoveTowards(_camera.position, _position, Speed * Time.deltaTime);
			if(_camera.position == _position)
			{
				_onFinished();
				_complete = true;
			}
		}
	}

	private class SwitchBridgeAction : LevelTesterAction
	{
		private Bridge _bridge;
		private VehicleType _type;

		public bool IsFinished
		{
			get; private set;
		}

		public SwitchBridgeAction(Bridge bridge, VehicleType type)
		{
			_bridge = bridge;
			_type = type;
		}

		public void StartAction(LevelTesterAI levelTester)
		{

		}

		public void DoAction()
		{
			GameObject obj = Game.instance.GetObjectAtPosition(_bridge.transform.position, _type);
			if(obj == null)
			{
				Debug.Log("Finished action");
				_bridge.SwitchState();
				IsFinished = true;
			}

			else if(obj.tag == "Bridge") Debug.Log("Object is bridge! Type: " + _type);
		}
	}

	private class BreakableBridgeFixAction : LevelTesterAction
	{
		private BreakableBridge _breakableBridge;

		public BreakableBridgeFixAction(BreakableBridge bridge)
		{
			_breakableBridge = bridge;
		}

		public bool IsFinished
		{
			get
			{
				return _breakableBridge.Health == BreakableBridge.BreakHealth;
			}
		}

		public void DoAction()
		{
			while(!IsFinished)
			{
				_breakableBridge.Repair();
			}
		}

		public void StartAction(LevelTesterAI levelTester)
		{

		}
	}
}
#endif