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

In addition to door, I want to have force field gates that are controlled by levers. I also want to have force field gate malfunctioning so that the force field goes on and off defined intervals (with random variation).

For force field itself, I need a shader that create force fieldish look. Luckily, there is something fist to this definition: http://wiki.unity3d.com/index.php?title=Shield.

Lets start with material for the force field. Figure below shows the set-up.

Force field material set-up.
Force field material set-up.

I also want to be able to warn when the force field is starting; eg. after malfunction I want to be able to give a visual hint that force field is starting. For that I create frame for the force field (in this case I created a door object with Prototype / ProCore Unity add-on). For the gate frame I create material that has emission material. I also add couple of boxes to as the right and left ends of the gate. Now I have gate object created and it looks as in Figure below.

Force field gate game object
Force field gate game object

ElectricPlane (should have renamed it to ForceField) needs two box colliders. One very narrow just covering the plane and one bigger that is set to trigger.

I also need a model for lever that can used to control the force field gate that has animated lever (the model & animations used in this example by Andreas Jansson). For the lever, I need an AnimatioController. Figure below shows the state machine and parameters. LevelAnimation and Backwards are connected to animations. NewState is an empty state so that nothing happens when scene starts. TiltForward is transition condition from Any State to LevelAnimation and TiltBackward to Backwards.

A lever AnimatorController
A lever AnimatorController

Now to implementation of the functionality. The gate should work in following cases

  1. The gate is controlled by a lever
  2. The gate is set off by the lever and it turns on after delay
  3. The gate is breaking and turns on and off within certain intervals with some random variation
  4. Combination of 1 & 2 (but this is not implemented in the code)

The cases 1, 2, 4 requires that gate has lever associated. 3 does not need lever and if it has the lever might not work correctly.

The gate turns immediately off, but when it is turned on there is always a small delay before gate turns on. So state changes are:

  • OFF -> STARTING -> ON
  • ON -> BREAKING -> STARTING -> ON  (and cycle starts from the beginning)
  • ON -> OFF

ElectricGateScript.cs:

using UnityEngine;
using System.Collections;


public class ElectricGateScript : MonoBehaviour {

    public enum GateType {LEVER, TIMED_LEVER, BREAKING, LEVER_AND_BREAKING};

    public GateType gateType;
    
    public int damage = 10;
    public int damageSpeed = 1;
    public AudioClip attackSound;
    public AudioClip idleSound;
    public AudioSource startingSound;	
    public Color inContactColor;
    public Renderer gateDoorRender;
    public float animSpeed = 0.1f;
    
    // after start, this amount sec goes before the force field is on
    public float poweringDuration = 3;

    // Used when gateType is TIMED_LEVER
    public float disableTime = 1; 

    // Used when gateType is BREAKING or LEVER_AND_BREAKING
    public float minTime = 1;
    public float maxTime = 2;

    private Renderer gateRenderer;
    private Color forceFieldOrigColor;

    // Keeping track of current gate states
    private enum GateStates {ON, OFF, BREAKING, STARTING};
    private GateStates state;
    private GateStates nextState;

    private AudioSource audioSource;
    private Renderer renderer;
	
    private LeverScript lever = null;
	
    private float nextOnOffDelay;
    private float gateMatLerp;
	
    private Collider[] colliders;

    private const float MAX_GATE_EMISSION = 0.8f;

    void Awake() {
        // Cashing components for the later use
        audioSource = GetComponent<AudioSource>();
        renderer = GetComponent<Renderer>();
        colliders = GetComponents<Collider>();
        gateRenderer = GetComponent<Renderer>();
        Color finalColor = Color.white * Mathf.LinearToGammaSpace (0);
        gateRenderer.material.SetColor ("_EmissionColor", finalColor);
        forceFieldOrigColor = renderer.material.GetColor("_Color");
    }

    void Start () {
    	state = GateStates.ON;
	if (startingSound != null) {
		Debug.LogError ("ElectricGateScript.Start(): Starting Sound is not set.");
	}
        StartGate (null);
    }

    public void StartGate(LeverScript _lever) {
	CancelInvoke ("HandleOnOff");
	lever = _lever;
	nextOnOffDelay = 0.0f;
	switch(gateType) {
	case GateType.LEVER:
		nextState = GateStates.STARTING;
		HandleStart();
		break;
	case GateType.TIMED_LEVER:
                nextState = GateStates.STARTING;
                HandleStart();
		break;
	case GateType.BREAKING:
		nextOnOffDelay = Random.Range(minTime + poweringDuration, maxTime + poweringDuration);
		HandleBreaking();
		break;
	case  GateType.LEVER_AND_BREAKING:
		Debug.LogError ("ElectricGate.StartGate(): gateType LEVER_AND_BREAKING not implemented");
		break;
		}
		
    }
	
    public void StopGate(LeverScript _lever) {
	Debug.Log ("ElectricGateScript.StopGate()");
	lever = _lever;
	state = nextState = GateStates.OFF;
	CancelInvoke ("HandleOnOff");
	CancelInvoke ("PowerinUpAnimation");
        if(gateType == GateType.TIMED_LEVER) {
              lever.OnWithDelay(disableTime);
        }
	TurnGateOff ();
    }

    public bool IsOn() {
	if(state == GateStates.OFF) {
		return false;
	}
	else {
		return true;
	}
    }
	
    private void HandleStart() {
	state = nextState;
	switch(state) {
	case GateStates.STARTING:
	        Powering ();
		nextState = GateStates.ON;
		Invoke ("HandleStart", poweringDuration);
		Powering ();
		break;
	case GateStates.ON:
		TurnGateOn();
		break;	
	}
     }
		
     private void HandleBreaking() {
	bool invokeNext = true;
		
	state = nextState;
		
	switch(state) {
		case GateStates.OFF:
			TurnGateOff ();
			invokeNext = false;
			break;
		case GateStates.BREAKING:
			TurnGateOff ();
			nextState = GateStates.STARTING;
			nextOnOffDelay = Random.Range(minTime, maxTime);
			break;
		case GateStates.STARTING:
			Powering ();
			nextState = GateStates.ON;
			nextOnOffDelay = poweringDuration;
			break;
		case GateStates.ON:
			TurnGateOn ();
			nextOnOffDelay = Random.Range(minTime, maxTime);
			nextState = GateStates.BREAKING;
			break;
	}

	Invoke ("HandleBreaking", nextOnOffDelay);
    }

    private void PowerinUpAnimation() {
	float step = 1.0f / (poweringDuration / 0.1f);
	gateMatLerp = gateMatLerp + step;
	float emission = Mathf.Lerp(0,1, gateMatLerp);
	Color finalColor = Color.white * Mathf.LinearToGammaSpace (emission);
	gateDoorRender.material.SetColor ("_EmissionColor", finalColor);
	if(gateMatLerp >= MAX_GATE_EMISSION) {
		CancelInvoke("PowerinUpAnimation");
	}
    }

    private void Powering () {
	if(startingSound) {
        	startingSound.Play();
        }
	gateMatLerp = 0;
	InvokeRepeating( "PowerinUpAnimation", 0.1f, 0.1f);
    }

    void TurnGateOn () {
	gameObject.SetActive(true);
	renderer.enabled = true;
	// for is more efficient to iterate through the list using for that foreach
	for(int i = 0; i < colliders.Length; i++ ) {
		colliders[i].enabled = true;
	}
        audioSource.Play();
	if (startingSound != null) {
		startingSound.Stop ();
	}
    }

    void TurnGateOff () {
	renderer.enabled = false;
        for (int i=0; i < colliders.Length; i++) {
			colliders[i].enabled = false;
        }
        StopAttack();
	audioSource.Stop();
	Color finalColor = Color.white * Mathf.LinearToGammaSpace (0);
	gateDoorRender.material.SetColor ("_EmissionColor", finalColor);
    }
    

    private void Animate () {
	if(state == GateStates.ON) {	
	   float emission = Mathf.Max (0.3f, Mathf.PingPong(Time.time, MAX_GATE_EMISSION));
	   Color finalColor = Color.white * Mathf.LinearToGammaSpace (emission);
	   gateDoorRender.material.SetColor ("_EmissionColor", finalColor);
	}
    }
    
    private void Attack () {
	audioSource.clip = attackSound;
	audioSource.loop = true;
	audioSource.Play();

	float emission = 1;
	Color finalColor = Color.white * Mathf.LinearToGammaSpace (emission);
	gateRenderer.material.SetColor ("_EmissionColor", finalColor);
	renderer.material.SetColor("_Color", inContactColor);

        // now I am assuming that only the PC 
        // can be damaged by the force field.
        GameAgents.GetCharacterInterface().ReceiveDamage(damage);
    }

    private void StopAttack () {
        CancelInvoke("Attack");
        
        audioSource.Stop ();
	audioSource.clip = idleSound;
	audioSource.loop = true;
	audioSource.Play();
		
	Color finalColor = Color.white * Mathf.LinearToGammaSpace (0);
	gateRenderer.material.SetColor ("_EmissionColor", finalColor);
	renderer.material.SetColor("_Color", forceFieldOrigColor);
    }

    void OnTriggerEnter (Collider other) {
        // now I am  assuming that only the PC 
        // can be damaged by the force field.
        if (other.gameObject == GameAgents.GetPlayer()) {
            InvokeRepeating("Attack", 0, damageSpeed);
        }
    }

    void OnTriggerExit () {
        StopAttack();
    }
    
}

Following figure show a set-up for the script as well as the gate object.

Force field gate script set-up and object
Force field gate script set-up and object

Lever object need also a box collider that is in trigger mode. Make sure that collider is big enough so that the PC can collide with that.

LeverScript.cs

using UnityEngine;
using System.Collections;

public class LeverScript : MonoBehaviour {

    public ElectricGateScript gate;
    public float leverAnimLen = 1.0f;
    
    private bool ableToActivate = true, whichAnimation = false;

    private Animator animator;
    private AudioSource audioSource;

    private float leverActived;

    void Awake() {
	if (gate == null) {
		Debug.LogError ("LevelScript.Awake(): Add Gate to " + name);
		ableToActivate = false;
	}
	animator = GetComponent ();
	if (animator == null) {
	        Debug.LogError ("LevelScript.Awake(): Animator component is missing from " + name);
		ableToActivate = false;
	}
	audioSource = GetComponent();
	leverActived = 0;
     }

    void OnTriggerEnter (Collider other) {
	if(other.gameObject == GameAgents.GetPlayer()) {
	    Debug.Log ("LeverScript.OnTriggerEnter() " + name);
            SwitchGate();
        }
    }

    private void SwitchGate () {
	if ( Time.time - leverActived < 1.0f ) {
		return;
	}
	if( IsInvoking() ) {
		CancelInvoke();
	}
	if(ableToActivate) {
		Debug.Log ("LeverScript.SwitchGate(): " + name);
		leverActived = Time.time;
	   	audioSource.Play ();
		PlayLeverAnimation();
		Invoke ("SwitchGateOnOff", leverAnimLen);
	}
	else {
		// Play not working sound
	}
    }


    private void SwitchGateOnOff () {
    	Debug.Log ("LeverScript.SwitchGateOnOff(): " + name);
	if(gate.IsOn()) {
		gate.StopGate(this);	
	}
	else {
		gate.StartGate(this);
	}
    }

    private void SwitchGateOn () {;
        PlayLeverAnimation();
        ableToActivate = true;
    }

    private void PlayLeverAnimation () {
        if (whichAnimation) {
        	animator.SetTrigger("TiltBackward");
	}
        else {
		animator.SetTrigger("TiltForward");
	}
        whichAnimation = !whichAnimation;
    }
    
    public void OnWithDelay(float delay) {
	Invoke ("SwitchGate", delay);
    }
}

Following figure shows a set-up for a lever.

Lever set-up
Lever set-up

I plan to start game so that the PC is in cell and after a short while the force field of the cell starts to break. To do that I need one more script that changes the cell gate from LEVER to BREAKING.

CellGateMalfunctions.cs:

using UnityEngine;
using System.Collections;

public class CellGateMalfunctions : MonoBehaviour {

	public ElectricGateScript cellGate;
	
	public float malfuntionStartDelay = 5.0f;
	public float malfunctionNextDelay = 2.0f;

	void Start () {
		Invoke ("StartMalfunction", malfuntionStartDelay);
	}
	
	public void StartMalfunction() {
		cellGate.gateType = ElectricGateScript.GateType.BREAKING;

		cellGate.StartGate(null);
		
	}
	
}

Attach the script to an empty game object and drag-and-drop ElectricPlane to CellGateMalfunctions script’s cellGate.

Previous parts of the tutorial

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 )

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: