Ley Lines
Engine: Unity 2018 ◆ Time: 7 weeks (2 week pre-production)
My role: Gameplay Programming and general Game Design
Ley Lines is a first person shooter/platformer in which you use your weapon, not only to vanquish enemies; but also to solve the puzzles and progress in the story.
Using a vast movement-set you can swiftly traverse the world both horizontally and vertically, use the environment as well as your gun “Ra” to open hidden path ways and bounce far out of the reach of enemies.
Spikey Block
Gas Pipe
Breakable Wall
Power Supply
Spikey Block
Gas Pipe
Breakable Wall
Power Supply
Spikey Block
Gas Pipe
Breakable Wall
Power Supply
Spikey Block
Gas Pipe
Breakable Wall
Power Supply
InteractableObject.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class InteractableObject : ShootableObject | |
{ | |
public override void ApplyDamage(float damageTaken, ElementalType type, | |
Tag damageSource, GameObject attackingObject) | |
{ | |
if (type == ElementalType.Ice) | |
{ | |
TakeIceDamage(); | |
} | |
else if (type == ElementalType.Fire) | |
{ | |
TakeFireDamage(); | |
} | |
else if (type == ElementalType.Default) | |
{ | |
TakeDefaultDamage(); | |
} | |
} | |
public virtual void TakeDefaultDamage() | |
{ | |
} | |
public virtual void TakeIceDamage() | |
{ | |
} | |
public virtual void TakeFireDamage() | |
{ | |
} | |
public virtual void SetPowerOn() | |
{ | |
} | |
public virtual void SetPowerOff() | |
{ | |
} | |
} |
SpikeyBlock.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class SpikeyFloor : InteractableObject | |
{ | |
protected AudioSource audioSource; | |
[SerializeField] | |
GameObject spikes; | |
[SerializeField] | |
GameObject iceBlock; | |
[SerializeField] | |
bool frozen = false; | |
ParticleSystem[] unfreezeParticleSystems; | |
bool move = true; | |
[HideInInspector] | |
public bool canDealDamage = true; | |
private bool moveUp = true; | |
protected bool oldMoveState = true; | |
private bool canTakeFireDamage = true; | |
Vector3 upPos = new Vector3(0f, -0.75f, 0f); | |
Vector3 fireShotUpPos = new Vector3(0f, 0f, 0f); | |
Vector3 downPos = new Vector3(0f, -1.9f, 0f); | |
Vector3 targetLerpPosition; | |
float lerpSpeed = 5f; | |
protected override void Start() | |
{ | |
audioSource = GetComponent<AudioSource>(); | |
unfreezeParticleSystems = GetComponentsInChildren<ParticleSystem>(); | |
spikes.transform.localPosition = upPos; | |
if (frozen) | |
{ | |
SetFrozen(); | |
} | |
} | |
private void Update() | |
{ | |
//Sets if it is in a state to deal damage to the player or not | |
if (!frozen && spikes.transform.localPosition.y >= (downPos.y * 0.9f)) | |
canDealDamage = true; | |
else | |
canDealDamage = false; | |
//Sets if its going up or down | |
if (Mathf.Approximately(spikes.transform.localPosition.y, upPos.y) || spikes.transform.localPosition.y > upPos.y) | |
moveUp = false; | |
else if (spikes.transform.localPosition == downPos) | |
moveUp = true; | |
//The actual movement of the spikes | |
if (move) | |
{ | |
if (oldMoveState != moveUp) | |
{ | |
if (moveUp) | |
{ | |
SoundManager.soundManager.PlaySpikeUpClip(audioSource); | |
} | |
else | |
{ | |
SoundManager.soundManager.PlaySpikeDownClip(audioSource); | |
} | |
oldMoveState = moveUp; | |
} | |
spikes.transform.localPosition = Vector3.Lerp(spikes.transform.localPosition, moveUp ? upPos : downPos, | |
Time.deltaTime * lerpSpeed); | |
} | |
} | |
//Handles the shots you fire at the object | |
IEnumerator ShotHitIsFire(bool fireHit) | |
{ | |
move = false; | |
targetLerpPosition = (fireHit ? fireShotUpPos : | |
(spikes.transform.localPosition.y > upPos.y ? fireShotUpPos : upPos)); | |
while (spikes.transform.localPosition != targetLerpPosition) | |
{ | |
spikes.transform.localPosition = Vector3.Lerp(spikes.transform.localPosition, | |
targetLerpPosition, Time.deltaTime * lerpSpeed * (fireHit ? 2f : 7f)); | |
yield return new WaitForEndOfFrame(); | |
} | |
if (frozen) | |
iceBlock.SetActive(true); | |
else | |
{ | |
yield return new WaitForSeconds(2f); | |
if (!frozen) | |
move = true; | |
} | |
} | |
//handles the lerp of the iceblock | |
IEnumerator SetFreezeIceBlock(bool freeze) | |
{ | |
if (!iceBlock.activeSelf) | |
{ | |
iceBlock.SetActive(true); | |
} | |
float lerpAlpha = 0f; | |
float lerpFrom = (freeze ? 0f : 1f); | |
float lerpTo = (freeze ? 1f : 0f); | |
while (lerpAlpha <= 1f) | |
{ | |
lerpAlpha += Time.deltaTime * (freeze ? 1f : 5f); | |
iceBlock.GetComponent<MeshRenderer>().material.SetFloat("_IsFrozen", Mathf.Lerp(lerpFrom, lerpTo, lerpAlpha)); | |
yield return new WaitForEndOfFrame(); | |
} | |
if (!freeze) | |
iceBlock.SetActive(false); | |
} | |
public override void TakeDefaultDamage() | |
{ | |
if (frozen) | |
{ | |
SetThawed(ElementalType.Default); | |
} | |
} | |
public override void TakeIceDamage() | |
{ | |
if (!frozen) | |
{ | |
SetFrozen(); | |
} | |
} | |
public override void TakeFireDamage() | |
{ | |
if (canTakeFireDamage) | |
{ | |
canTakeFireDamage = false; | |
Invoke("SetCanTakeFireDamage", 1); | |
if (frozen) | |
SetThawed(ElementalType.Fire); | |
else | |
StartCoroutine(ShotHitIsFire(true)); | |
} | |
} | |
private void SetCanTakeFireDamage() | |
{ | |
canTakeFireDamage = true; | |
} | |
void SetFrozen() | |
{ | |
frozen = true; | |
StartCoroutine(ShotHitIsFire(false)); | |
StartCoroutine(SetFreezeIceBlock(true)); | |
} | |
void SetThawed(ElementalType elementHit) | |
{ | |
if (elementHit == ElementalType.Default) | |
{ | |
iceBlock.SetActive(false); | |
foreach (ParticleSystem ps in unfreezeParticleSystems) | |
{ | |
ps.Play(); | |
} | |
} | |
else | |
{ | |
StartCoroutine(SetFreezeIceBlock(false)); | |
} | |
frozen = false; | |
move = true; | |
} | |
} |
GasPipe.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class GasPipe : InteractableObject | |
{ | |
protected AudioSource audioSource; | |
[SerializeField] | |
State currentState = State.Unbroken; | |
[SerializeField] | |
GameObject fireCloud; | |
[SerializeField] | |
GameObject iceBlock; | |
MeshRenderer iceBlockRenderer; | |
//This if for use if the level designer wants the fire to activate in bursts or just be static | |
[SerializeField] | |
bool useTimeLimits = true; | |
[SerializeField] | |
float activeTimeLimit = 3f; | |
[SerializeField] | |
float nonActiveTimeLimit = 2f; | |
private float currentTimer = 0f; | |
[HideInInspector] | |
public bool emitterIsActive = false; | |
private bool canTakeFireDamage = true; | |
private enum State | |
{ | |
Unbroken, | |
Ice, | |
Fire | |
} | |
protected override void Start() | |
{ | |
audioSource = GetComponent<AudioSource>(); | |
iceBlockRenderer = iceBlock.GetComponent<MeshRenderer>(); | |
SetState(currentState); | |
} | |
private void Update() | |
{ | |
if (useTimeLimits && currentState == State.Fire) | |
{ | |
currentTimer += Time.deltaTime; | |
if (currentTimer >= (emitterIsActive ? activeTimeLimit : nonActiveTimeLimit)) | |
{ | |
SetFireActive(!emitterIsActive); | |
} | |
} | |
} | |
public override void TakeDefaultDamage() | |
{ | |
if (currentState == State.Unbroken || currentState == State.Ice) | |
SetState(State.Fire); | |
} | |
public override void TakeIceDamage() | |
{ | |
if (currentState == State.Fire || currentState == State.Unbroken) | |
SetState(State.Ice); | |
} | |
public override void TakeFireDamage() | |
{ | |
if (currentState == State.Ice || currentState == State.Unbroken) | |
SetState(State.Fire); | |
} | |
//Custom material made by the tech artist that gradually freezes or unfreezes | |
IEnumerator SetFreezeIceBlock(bool freeze) | |
{ | |
if (!freeze && currentState == State.Unbroken) | |
yield break; | |
if (!iceBlock.activeSelf) | |
iceBlock.SetActive(true); | |
float lerpAlpha = 0f; | |
float lerpFrom = (freeze ? 0f : 1f); | |
float lerpTo = (freeze ? 1f : 0f); | |
while (lerpAlpha <= 1f) | |
{ | |
lerpAlpha += Time.deltaTime * (freeze ? 2f : 5f); | |
iceBlock.GetComponent<MeshRenderer>().material.SetFloat("_IsFrozen", Mathf.Lerp(lerpFrom, lerpTo, lerpAlpha)); | |
yield return new WaitForEndOfFrame(); | |
} | |
if (!freeze) | |
iceBlock.SetActive(false); | |
} | |
private void SetState(State newState) | |
{ | |
switch (newState) | |
{ | |
case State.Fire: | |
{ | |
audioSource.Play(); | |
StartCoroutine(SetFreezeIceBlock(false)); | |
currentState = State.Fire; | |
SetFireActive(true); | |
break; | |
} | |
case State.Ice: | |
{ | |
audioSource.Stop(); | |
StartCoroutine(SetFreezeIceBlock(true)); | |
currentState = State.Ice; | |
SetFireActive(false); | |
break; | |
} | |
case State.Unbroken: | |
{ | |
SetFireActive(false); | |
break; | |
} | |
} | |
} | |
private void SetFireActive(bool newActive) | |
{ | |
currentTimer = 0f; | |
emitterIsActive = newActive; | |
SetEmitterActive(newActive); | |
} | |
private void SetEmitterActive(bool active) | |
{ | |
if (active) | |
fireCloud.GetComponent<ParticleSystem>().Play(); | |
else if (!active) | |
fireCloud.GetComponent<ParticleSystem>().Stop(); | |
} | |
} |
BreakableWall.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class BreakableWall : InteractableObject | |
{ | |
AudioSource audioSource; | |
float directionMultiplier; | |
Vector3 directionToTarget = new Vector3(); | |
Material frozenMat; | |
[SerializeField] | |
float explosionForce = 500f; | |
bool canTakeFireDamage = true; | |
Rigidbody[] rigidbodies; | |
GameObject attackingGameObject; | |
public override void ApplyDamage(float damageTaken, ElementalType type, Tag damageSource, GameObject attackingObject) | |
{ | |
attackingGameObject = attackingObject; | |
base.ApplyDamage(damageTaken, type, damageSource, attackingObject); | |
} | |
protected override void Start() | |
{ | |
rigidbodies = GetComponentsInChildren<Rigidbody>(); | |
audioSource = GetComponent<AudioSource>(); | |
} | |
public override void TakeFireDamage() | |
{ | |
if (canTakeFireDamage) | |
{ | |
canTakeFireDamage = false; | |
WallExplode(); | |
} | |
} | |
//For this to look good and work as intended I needed to apply force to each individual piece of the wall | |
void WallExplode() | |
{ | |
GetComponent<Collider>().enabled = false; | |
//calculates the direction to the player and explodes away from the player | |
//(No matter what angle the player shoots the wall at it looks nice) | |
directionToTarget = transform.position - attackingGameObject.transform.position; | |
float dot = Vector3.Dot(directionToTarget, transform.up); | |
directionMultiplier = (dot < 0 ? -1f : 1f); | |
for (int i = 0; i < rigidbodies.Length; i++) | |
{ | |
rigidbodies[i].isKinematic = false; | |
rigidbodies[i].AddForce(rigidbodies[i].transform.up * explosionForce * directionMultiplier); | |
} | |
SoundManager.soundManager.PlayDestructableWallClip(audioSource); | |
} | |
} |
PowerSupply.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
public class PowerSupply : InteractableObject | |
{ | |
protected AudioSource audioSource; | |
private float startDelay = 0.1f; | |
//Materials for the PS | |
[SerializeField] | |
Material activeMaterial; | |
[SerializeField] | |
Material frozenMaterial; | |
MeshRenderer meshRenderer; | |
[SerializeField] | |
List<InteractableObject> affectedInteractableObjects = new List<InteractableObject>(); | |
//The state | |
[SerializeField] | |
State currentState; | |
private enum State | |
{ | |
Active, | |
Inactive, | |
} | |
protected override void Start() | |
{ | |
audioSource = GetComponent<AudioSource>(); | |
meshRenderer = gameObject.GetComponent<MeshRenderer>(); | |
if (currentState == State.Inactive) | |
Invoke("SetStateInactive", startDelay); | |
else | |
Invoke("SetStateActive", startDelay); | |
} | |
//What happens when damage is dealt | |
public override void TakeDefaultDamage() | |
{ | |
if (currentState == State.Inactive) | |
SetStateActive(); | |
} | |
public override void TakeIceDamage() | |
{ | |
if (currentState == State.Active) | |
SetStateInactive(); | |
} | |
//Set states | |
private void SetStateActive() | |
{ | |
SoundManager.soundManager.PlayFuseboxActivationClip(audioSource); | |
currentState = State.Active; | |
foreach (InteractableObject interactableObject in affectedInteractableObjects) | |
{ | |
interactableObject.SetPowerOn(); | |
} | |
meshRenderer.material = activeMaterial; | |
} | |
private void SetStateInactive() | |
{ | |
currentState = State.Inactive; | |
foreach (InteractableObject interactableObject in affectedInteractableObjects) | |
{ | |
interactableObject.SetPowerOff(); | |
} | |
meshRenderer.material = frozenMaterial; | |
} | |
} |