Intro to Gameplay Programming with Unity

This post contains an solutions  to my Unity programming exercises and classes I showed at the lectures. This post cover coding following game logic:

  • a waypoint track
  • an agent following a waypoint track
  • display two meters  for energy and health
  • collectable powerups for gaining health and power

Waypoint (C#)

Class creates an waypoint that can be connected to other waypoint. The script should be attached to a game object (without renderer) that has Collider set as a trigger. When a agent with the same tag as in agentTag, the class class a script Enemy.SetTarget(Transform nextTarget) colliding game object to give the next target for the agent. In order to see the waypoint, Assets/Gizmos folder should have a small (e.g., 32×32) figure named waypoint.psd (or something else if you change the name in the code). The connection between waypoints are also visualized.

public class Waypoint : MonoBehaviour {
	public Transform nextTarget;
	public string agentTag;

	void OnTriggerEnter(Collider other) {
		Debug.Log("Waypoint.OnTriggerEnter(): " + other.gameObject.name + " triggered " + name);
		if(other.gameObject.CompareTag(agentTag)) {
			SendNextTarget(other.gameObject);
		}
	}

	void OnDrawGizmos() {
		Gizmos.DrawIcon (transform.position, "waypoint.psd");
		if(nextTarget) {
			Gizmos.color = Color.red;
			Gizmos.DrawLine(transform.position, nextTarget.position);
		}
	}

	private void SendNextTarget(GameObject obj) {
		Enemy script = obj.GetComponent<Enemy>();
		if(script) {
			script.SetTarget(nextTarget);
		}
		else {
			Debug.LogError("Waypoint.SendNextTarget() " + obj.name + " has no Enemy script component.");
		}
	}
}

Enemy (C#)

This script should be attached to a game object that has a CharacterController attached to it. In inspector set the tag to match the agentTag value of waypoint group the object should be following. Tag is used here to make possible that we have different tracks that the agents are following, but without need to worry if their paths crosses a waypoint of the other track. The first waypoint should be connected in inspector by draging it to target.

public class Enemy : MonoBehaviour {

	public Transform target;
	public float speed = 3.0f;

	void Update () {
		if(target) {
			transform.LookAt(new Vector3(target.position.x, transform.position.y, target.position.z)));
			transform.Translate(0,0,speed*Time.deltaTime);
		}
	}

	public void SetTarget(Transform newTarget) {
		target = newTarget;
	}

GameAgents (C#)

This a class that offers easy access to player controlled game object (and possibly later to other game objects) and scripts. This should be attached to some game object, but only to one (this is singleton). The script assumes that a scene contains object tagged as “Player” and that object has PlayerLogic component.

public class GameAgents : MonoBehaviour {

	private static GameAgents instance;
	private GameObject player;
	private PlayerLogic playerLogic;

	void Awake () {
		instance = this;
		player = GameObject.FindWithTag("Player");
		if(!player) {
			Debug.LogError("GameAgents.Awake(): Cannot find GameObject with tag player");
		}
		playerLogic = player.GetComponent<PlayerLogic>();
		if(!playerLogic) {
			Debug.LogError("GameAgents.Awake(): Player object does not contain PlayerLogic component");
		}
	}
	public static GameObject GetPlayer() { return instance.player; }
	public static PlayerLogic GetPlayerLogic() { return instance.playerLogic; }
}

PlayerLogic (C#)

Here I have a PlayerLogic stub that have energy and health that can go from 0 to 100. I display these stats using Unity GUI system. The class contains function for handling powerups collecting powerups or traps/bombs (PowerUp class will be below). I attached this script to FirstPersonController, but the script and game logic with PowerUp works if the game object in which the PlayerLogic is attached has CharacterController. (The stats energy and health are public just for making testing easy). Now, when health goes down to zero, controls are disabled, but no other feedback is given.

public class PlayerLogic : MonoBehaviour {

	public Texture2D energyBarBackground;
	public Texture2D energyBar;
	public Texture2D healthBar;
	public int energy = 100;
	public int health = 100;

	private int maxEnergy = 100;
	private int maxHealth = 100;

	void OnGUI() {
		GUI.matrix = GUIGlobals.GetGUIMatrix();
		GUI.DrawTexture(new Rect(50, GUIGlobals.screenHeight - 300, 40, 208), energyBarBackground);
		GUI.DrawTexture(new Rect(52, GUIGlobals.screenHeight - 96 - energy*2, 32, energy*2), energyBar);
		GUI.DrawTexture(new Rect(GUIGlobals.screenWidth - 50, GUIGlobals.screenHeight - 300, 40, 208), energyBarBackground);
		GUI.DrawTexture(new Rect(GUIGlobals.screenWidth - 48, GUIGlobals.screenHeight - 96 - health*2, 32, health*2), healthBar);
	}	

	public void PowerUp(int _energy, int _health) {
		energy += _energy;
		if(energy > maxEnergy) {
			energy = maxEnergy;
		}
		health += _health;
		if(health > maxHealth) {
			health = maxHealth;
		}
                else if(health <= 0) {
                         Debug.Log("PlayerLogic.PowerUp(): Health reached to zero");
                         MouseLook mouseLook = GetComponent<MouseLook>();
                         FPSInputController fpsInputController = GetComponent<FPSInputController>();

                         mouseLook.enabled = false;
                         fpsInputController.enabled = false;
                }
                Debug.Log("PlayerLogic.PowerUp(" + _energy + ", " + _health + "): New power: " + energy + ", new health: " + health);
	}
}

PowerUp (C#)

This script should be attached to a game object with collider set as trigger.  If the player object colliders with it we call its PowerUp fuction to sent amount of energy and health the power up has (note that you can create traps/bombs with negative values). After collision, the powerup destroys itself to prevent collecting it multiple times. Here I do not use tag comparison to check if the colliding object is Player, because string comparison is slower that object comparison (and I have already cached the player object in GameAgents script).

public class PowerUp : MonoBehaviour {

	public int energy = 10;
	public int health = 10;

	void OnTriggerEnter(Collider other) {
		Debug.Log("PowerUp.OnTriggerEnter(): collision with " + other.gameObject.name);
		if(other.gameObject == GameAgents.GetPlayer()) {
			GameAgents.GetPlayerLogic().PowerUp(energy, health);
			audio.Play();
			// Sounds won't play if we destroy the PowerUp immediately.
			Invoke("SelfDestruct", 0.2f);
		}
	}

	private void SelfDestruct() {
		Destroy(gameObject);
	}
}

The functionality of implemented in these classes far from complete set, but they intend to give idea how to handle class-to-class communication to implement couple of very typical game features. In the game programming course, next we look at how to make Enemy perceive and attack if player object is visible and not too far away.

(The post is is licensed under a Creative Commons Attribution 3.0 Unported License).

Published by lankoski

Petri Lankoski, D.Arts, is a Associate Professor in Game Studies at the school of Communication, Media and IT at the Södertörn University, Sweden. His research focuses on game design, game characters, role-playing, and playing experience. Petri has been concentrating on single-player video games but researched also (multi-player) pnp and live-action role-playing games. This blog focuses on his research on games and related things.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: