Tutorial: 1st-person sneak in Unity 5, part 4

This part will add doors leading a level to another and a logic for adding/moving the PC object in the correct place on the scene.

Previous parts of tutorial

New functionality that I intent to introduce require adding logic to the PC. Lets start that by creating minimal implement of the CharacterInterface. First, I add features I need to implement doors leading from a level to another. I set-up door object so that each door has a spawn point attached to each other (see figure below) and that door is in two levels: one side in one and the other side in the other. (I am using scifi door from Unity assets store by 3DMondra as model in the image).

Door set-up: spawn point
Door set-up: spawn point

PlayerLogic.cs

using UnityEngine;
using System.Collections;

public class PlayerLogic : MonoBehaviour, CharacterInterface {

	public const string CURRENT_LEVEL_KEY = "c";
	public const string LAST_DOOR_KEY = "d";

	bool CharacterInterface.CanUseHealth() { 
             return true;
        }

	bool CharacterInterface.CanUseEnergy() {
             return true;
        }

	void CharacterInterface.Save(string levelName, string door) {
                // save will be used to keep track which level we are in and
                // and which door was used to come to that level 
		Debug.Log ("PlayerLogic.Save(" + levelName + ")");
		PlayerPrefs.SetString(CURRENT_LEVEL_KEY, levelName);
		PlayerPrefs.SetString(LAST_DOOR_KEY, door);
	}

    void Start() {
       	Transform startPosition = null;
	string door = PlayerPrefs.GetString(LAST_DOOR_KEY);
	Debug.Log ("PlayerLogic.OnLevelWasLoaded(): door = " + door);
	if (door != "") {
                // door was used when the door has value. Now we need to find the door
		GameObject doorObj = GameObject.Find(door);
		if (doorObj) {
                        // and child object called spawn that indicate where we should spawn
                        // the player object.
		        startPosition = doorObj.transform.Find("spawn");
			if(startPosition == null) {
	                	Debug.LogError ("PlayerLogic.OnLevelWasLoaded(): " + doorObj.name + " has now spawn point");
			}
		}
	}
		
	if(startPosition == null) {
		// if we did not come via door, we use object with tag PlayerStartPosition.    
        	GameObject ps = GameObject.FindWithTag("PlayerStartPoint");
        	if (startPosition) {
			startPosition = ps.transform;
        	}
        }
        // now lets place and orient the PC based on the found start position object. 
        transform.rotation = startPosition.transform.rotation;
        transform.position = startPosition.position;
    }

    bool CharacterInterface.IsInvisible () {
        return false;
    }

    void CharacterInterface.ReceiveDamage (int amount) {

    }

    void CharacterInterface.PowerUp (int _energy, int _health) {

   }
}

As you noticed, I do not want to place the PC directly in the scenes by place PC based on doors used to come to the level or StartPositon objects. For this we need to have a manager that instantiate the PC when needed. Lets create empty GameObject and call it GameManager. I create GameAgents script that handles instantiating the PC using prefab. GameAgent is singleton. That means that there can be only one instance of the script in the scene. Multiple instances will lead problems.

[EDIT1, added functionality for handling door randomisation, see below.]

GameAgents.cs

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class GameAgents : MonoBehaviour {
	
	public bool debug = false;
	
	public GameObject playerPrefab;
	
	public Slider energyBar;
	public Slider healthBar;
	
	public Button debugNewButton;
	public Text debugSqnText;
	
	private static GameAgents instance;
	
	private GameObject player;
	private CharacterInterface characterInterface;
	
	private const string SAVE_SQN_KEY = "sqn";
	
	// Use this for initialization	
	void Awake () {
		Debug.Log("GameAgents.Awake()");
		if(instance) {
			Debug.Log("GameAgents.Awake(): Warning, GameAgents unity singleton already initialized");	
		}
		GameAgents[]objs = FindObjectsOfType();
		if(objs.Length != 1) {
			Debug.LogError ("GameAgents.Awake(): Multiple GameAgents in scene.");	
		}
		instance = objs[0];
		
		player = GameObject.FindWithTag("Player");
		if(!player) {
			player = Instantiate(playerPrefab);	
		}
		characterInterface = player.GetComponent();
		if(characterInterface == null) {
			Debug.LogError("GameAgents.Awake(): Player object does not contain CharacterInterface component");
		}
		
		PlayerLogic pc = player.GetComponent();
		pc.energyBar = energyBar;
		pc.healthBar = healthBar;
		
		debugNewButton.gameObject.SetActive(debug);
	}
	
	public void NewGameDebug() {
		PlayerLogic.DeleteSave();
		GameAgents.NewSGameSqn();
		PlayerLogic pc = player.GetComponent();
		pc.ReadSave();
		debugSqnText.text = GameAgents.GameSqn().ToString();
	}
	
	public static GameObject GetPlayer() {
		return instance.player;	
	}
	
	public static CharacterInterface GetCharacterInterface() {
		return instance.characterInterface;	
	}
	
	public static void NewSGameSqn() {
		int sqn = PlayerPrefs.GetInt(SAVE_SQN_KEY, 0);
		sqn++;
		PlayerPrefs.SetInt(SAVE_SQN_KEY, sqn);
		PlayerPrefs.Save ();
	}
	
	public static int GameSqn() {
		return PlayerPrefs.GetInt(SAVE_SQN_KEY, 0);
	}
}


Door does not work yet. We need to add Collider in trigger mode to it, add script that catches the trigger event and knows what level should be loaded. Door should also have an AudioSource component that has a sound file attached that can be played when the door is used.

[EDIT1: refactored door to have better support randomising level structure, but keeping the structure stable within one game.]
Door.cs

using UnityEngine;
using System.Collections;

[System.Serializable]
public class DoorLink {
	public int index;   
	public string level; // the level where this door leads
	public string door;  // the name of door in the level 
}

public class Door : MonoBehaviour {

	public DoorLink[] doorLink;

	AudioSource audioSource;
	public int keyId = 0;

	//private int doorLinkIndex;
	private DoorLink activeLink;
	
	private const string DOOR_LINK_INDEX_KEY = "doi";

	void Awake() {
		if (doorLink.Length <= 0) {
			Debug.LogError ("Door.Awake(): You need have at least 1 exit for " + name);
		}
		audioSource = GetComponent ();
	}

	void Start() {
		string sqn_key = System.String.Format("{0}_sqn", name);
		int doorLinkIndex = PlayerPrefs.GetInt(DOOR_LINK_INDEX_KEY, -1);
		if(GameAgents.GameSqn() > PlayerPrefs.GetInt(sqn_key) || doorLinkIndex == -1) {
			
			doorLinkIndex = Random.Range(0,doorLink.Length);
			PlayerPrefs.SetInt(DOOR_LINK_INDEX_KEY, doorLinkIndex);
			activeLink = doorLink[doorLinkIndex];
		}
		else {
			activeLink = null;
			for(int n = 0; n< doorLink.Length; n++) {
 				if(doorLink[n].index == doorLinkIndex) {
 					activeLink = doorLink[n];
 					break; 				} 			} 			if(activeLink == null) {
 				Debug.LogError("Door.Start(): cant find Door Link with index " + doorLinkIndex + " door does not lead back to the previous level"); 			} 		} 		Debug.Log("Door.Start(): " + name + " -> " + activeLink.level + " ( door = " + activeLink.door +  ", index = " + activeLink.index +  " )" );
	}
	
	void OnTriggerEnter(Collider other) {
		if(other.gameObject.tag == "Player") {
			Debug.Log("Door.LoadLevel(): loading level " + activeLink.level + " ( door = " + activeLink.door + " )");
			CharacterInterface ci = other.gameObject.GetComponent();
			if(ci != null) {
				if(keyId == 0 || ci.HasKey(keyId)) {
					ci.Save(activeLink.level, activeLink.door);
					audioSource.Play();
					Application.LoadLevel(activeLink.level);
					PlayerPrefs.Save ();
				}
				else {
					Debug.Log ("Door.OnTriggerEnter(): " + name + " is locked");
					// Display that the door is locked.
				}
			}
			else {
				Debug.LogError("Door.OnTriggerEnter() GameObject has tag Player, but doens not have CharacterInterface");
			}
		}
	}
	
}

Now we need to have two levels, lets say level_start and level1. Both levels should also have an empty GameObject tagged as PlayerStartPoint so that the PC gets spawned somewhere before doors have been used first time (or the level does not have a correct door, like when hitting play in some level). Figure below shows a door setup for a door that have two randomly selected alternatives where the door can lead.

Door setup. Two possible different end points in level1. There are selected randomly (but are persistent within one play-through). Note that this example will create non-functioning one door to level1. The correct use would be only one door counter-part in one level.
Door setup. Two possible different end points in level1. There are selected randomly (but are persistent within one play-through). Note that this example will create non-functioning one door to level1. The correct use would be only one door counter-part in one level.

After this we need two doors at level1. Door1 and Door2. The DoorLink of the doors at level1 should be as follows:
Door1

  • Index: 0
  • Level: level_start
  • Door: Door1

Doo2

  • Index: 1
  • Level: level_start
  • Door: Door1

Note that when a door have multiple DoorLinks, the doors in different levels needs to have the same name. Otherwise the door might not lead back to the same place (even if the door is otherwise correctly configured).

Advertisements

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 )

Google+ photo

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

Connecting to %s