Shattered Veil

Game Project 2 Summary

This was the last project in Unity with a large team of 20 people. And as expected, there were quite a number of communication issues that came up throughout the project's development. From multiple ideas being interpreted in their own ways, to somehow the game becoming an Action FPS.

The game was supposed to be a narrative-driven exploration game with a touch of combat. And the world is based on Celtic mythology. We also had the restriction of having some mobile compatibility, which we struggled a lot with because our game barely launched on mobile.

Check out the game here:
Shattered Veil

YouTube video preview image

My Contributions

Enemy AI

One of my responsibilities was implementing different enemies that the player will encounter as the game progresses. At the end of production, I had made two types of basic enemies, one melee and the other being ranged. Lastly, we had a boss enemy. While developing the AI, I found that making interesting behaviors is not as simple as I thought in the beginning.

public class EnemyMeleeBehaviourTree : BehaviourTree
{
Animator enemyAnimator;
GameObject player;
NavMeshAgent agent;
EnemyAIStats enemyMeele;
public override void StartEnemyBehabiour()
{
base.StartEnemyBehabiour();

myBlackBoard = new BlackBoard();
player = GameObject.FindGameObjectWithTag("Player");
agent = GetComponent<NavMeshAgent>();
enemyMeele = GetComponent<EnemyAIStats>();
enemyAnimator = GetComponentInChildren<Animator>();

myRootNode =
new Sequence(new List<BehaviourNode>
{
new IsStuned(new List<BehaviourNode>()),

new Selector(new List<BehaviourNode>
{
new CanSeePlayer(new List<BehaviourNode>()),
new Patrol(new List<BehaviourNode>())
}),
new ChasePlayer(new List<BehaviourNode>()),
new AttackPlayer(new List<BehaviourNode>()),
});

myBlackBoard.data.Add("PlayerGameObject", player);
myBlackBoard.data.Add("ThisEnemyTransform", transform);
myBlackBoard.data.Add("EnemyAgent", agent);
myBlackBoard.data.Add("EnemyMeele", enemyMeele);
myBlackBoard.data.Add("EnemyAnimator", enemyAnimator);

myRootNode.PopulateBlackBoard(myBlackBoard);
}

void Update()
{
UpdateTree();
enemyAnimator.SetFloat("Speed", agent.velocity.magnitude / agent.speed);
}
}
public class CanSeePlayer : BehaviourNode
{
public CanSeePlayer(List<BehaviourNode> someChildren) : base(someChildren)
{

}

float timer = 0f;

public override ReturnState Evaluate()
{
GameObject player = myBlackBoard.data["PlayerGameObject"] as GameObject;
Transform enemyTransform = myBlackBoard.data["ThisEnemyTransform"] as Transform;
NavMeshAgent agent = myBlackBoard.data["EnemyAgent"] as NavMeshAgent;
EnemyAIStats enemy = myBlackBoard.data["EnemyMeele"] as EnemyAIStats;

if (myBlackBoard.data.TryGetValue("StoppingDistance", out object value))
{
agent.stoppingDistance = float.Parse(value.ToString());
}

if (player != null)
{
float distanceToPlayer = Vector3.Distance(enemyTransform.position, player.transform.position);
if (distanceToPlayer < enemy.SightDistance)
{
Vector3 targetDirection = player.transform.position - enemyTransform.position - (Vector3.up * enemy.EyeHeight);
float angleToPlayer = Vector3.Angle(targetDirection, enemy.transform.forward);

if (angleToPlayer >= -enemy.FielOfView && angleToPlayer <= enemy.FielOfView)
{
Ray ray = new Ray(enemyTransform.position + (Vector3.up * enemy.EyeHeight), targetDirection);
RaycastHit hitInfo = new RaycastHit();

if (Physics.Raycast(ray, out hitInfo, enemy.SightDistance))
{
if (hitInfo.transform.gameObject == player)
{
RotateEnemyToPlayer(enemy, player);

Debug.DrawRay(ray.origin, ray.direction * enemy.SightDistance);
timer = 0f;
return ReturnState.Success;
}
else if (timer < enemy.LooseSightTime)
{
RotateEnemyToPlayer(enemy, player);

Debug.DrawRay(ray.origin, ray.direction * enemy.SightDistance);
timer += Time.deltaTime;
return ReturnState.Success;
}
}
}
else if (timer < enemy.LooseSightTime)
{
RotateEnemyToPlayer(enemy, player);

timer += Time.deltaTime;
return ReturnState.Success;
}
}
else if (timer < enemy.LooseSightTime)
{
RotateEnemyToPlayer(enemy, player);

timer += Time.deltaTime;
return ReturnState.Success;
}
else
{
myBlackBoard.data["PlayerOutOfSight"] = true;
return ReturnState.Failure;
}
}

myBlackBoard.data["PlayerOutOfSight"] = false;
return ReturnState.Failure;
}

public void RotateEnemyToPlayer(EnemyAIStats enemy, GameObject player)
{
Quaternion targetRotation = Quaternion.LookRotation(new Vector3(player.transform.position.x - enemy.transform.position.x, 0, player.transform.position.z - enemy.transform.position.z), Vector3.up);
enemy.transform.rotation = Quaternion.Slerp(enemy.transform.rotation, targetRotation, Time.deltaTime * enemy.RotationSpeed);

}
}
public class ChasePlayer : BehaviourNode
{
private EventInstance walkSoundInstance;
private bool isWalking = false;


public ChasePlayer(List<BehaviourNode> someChildren) : base(someChildren)
{
walkSoundInstance = RuntimeManager.CreateInstance("event:/SFX/Enemy_WalkRun");

EnemyAIStats.OnStopEnemySound += StopEnemySound;
}

float newDestinationCoolDown = 0.5f;

public override ReturnState Evaluate()
{
GameObject player = myBlackBoard.data["PlayerGameObject"] as GameObject;
NavMeshAgent agent = myBlackBoard.data["EnemyAgent"] as NavMeshAgent;
Animator enemyAnim = myBlackBoard.data["EnemyAnimator"] as Animator;


if (player != null)
{
if (newDestinationCoolDown <= 0)
{
newDestinationCoolDown = 0.5f;
agent.SetDestination(player.transform.position);

if (!isWalking && myBlackBoard.data.ContainsKey("PlayerOutOfSight"))
{
walkSoundInstance.start();
isWalking = true;
}
}
newDestinationCoolDown -= Time.deltaTime;

if (myBlackBoard.data.ContainsKey("PlayerOutOfSight") && (bool)myBlackBoard.data["PlayerOutOfSight"])
{
StopEnemySound();

}

return ReturnState.Success;
}
else
{
StopEnemySound();
return ReturnState.Failure;
}
}
public void StopEnemySound()
{
if (isWalking)
{
walkSoundInstance.stop(FMOD.Studio.STOP_MODE.ALLOWFADEOUT);
isWalking = false;
}
}

~ChasePlayer()
{
EnemyAIStats.OnStopEnemySound -= StopEnemySound;
}
}

Color Puzzle

Another feature I implement is the color puzzle. It is where you need to put the specific colored key into the same lock. With some locks requiring multiple keys. And if you solved the puzzle, the three types of doors would open.

public enum DoorType
{
NormalDoor,
StoneDoor,
FogDoor
}

public class RitualStoneDoor : MonoBehaviour
{
public DoorType DoorType;
public AnimationCurve DoorOpeningAnimationSpeed;
public Transform NormalDoorPivotPoint;
public float OpenDoorRotationAngle = 100f;
public List<GameObject> ConditionObjects;

private ParticleSystem fogParticles;
private Collider fogCollider;
private Animator rockDoorAnimator;
private int completedObjects = 0;
private RotationDirection direction;

private void Start()
{
if (OpenDoorRotationAngle > 0)
{
direction = RotationDirection.PositiveNums;
}
else
{
direction = RotationDirection.NegativeNums;
}
}

public void TryOpenDoor()
{
completedObjects = 0;
for (int i = 0; i < ConditionObjects.Count; i++)
{
if (ConditionObjects[i].TryGetComponent<RitualStoneLock>(out RitualStoneLock TheLock))
{
if (TheLock.Completed == true)
{
completedObjects++;
}
}
}

if (completedObjects == ConditionObjects.Count)
{
switch (DoorType)
{
case DoorType.NormalDoor:
StartCoroutine(DoorOpenInProgress());

break;
case DoorType.StoneDoor:
rockDoorAnimator = gameObject.GetComponent<Animator>();
rockDoorAnimator.SetTrigger("Open");

break;
case DoorType.FogDoor:
fogParticles = gameObject.GetComponent<ParticleSystem>();
fogCollider = gameObject.GetComponent<Collider>();
fogCollider.enabled = false;
var tempMain = fogParticles.main;
tempMain.loop = false;
tempMain.startLifetime = 0.5f;
tempMain.simulationSpeed = 3f;

break;
}
}
else
{
Debug.Log("Not all conditions are met");
}
}

public IEnumerator DoorOpenInProgress()
{
switch (direction)
{
case RotationDirection.PositiveNums:
while (NormalDoorPivotPoint.localEulerAngles.y + 0.0001f < OpenDoorRotationAngle - (Math.Round(OpenDoorRotationAngle / 360) * 360))
{
NormalDoorPivotPoint.localRotation = Quaternion.Lerp(NormalDoorPivotPoint.localRotation, Quaternion.Euler(new Vector3(0, OpenDoorRotationAngle, 0)), DoorOpeningAnimationSpeed.Evaluate(Time.deltaTime));
yield return null;
}

break;
case RotationDirection.NegativeNums:

while (NormalDoorPivotPoint.localEulerAngles.y - 0.0001f > (OpenDoorRotationAngle - (Math.Round(OpenDoorRotationAngle / 360) * 360)) + 360 || NormalDoorPivotPoint.localEulerAngles.y <= 0f)
{
NormalDoorPivotPoint.localRotation = Quaternion.Lerp(NormalDoorPivotPoint.localRotation, Quaternion.Euler(new Vector3(0, OpenDoorRotationAngle, 0)), DoorOpeningAnimationSpeed.Evaluate(Time.deltaTime));
yield return null;
}

break;
}
}

private void OnDrawGizmos()
{
if (DoorType == DoorType.NormalDoor)
{
Gizmos.color = Color.red;
var direction = Quaternion.AngleAxis(OpenDoorRotationAngle, transform.up) * NormalDoorPivotPoint.forward;
Gizmos.DrawRay(NormalDoorPivotPoint.position, direction * 2f);
Gizmos.color = Color.blue;
Gizmos.DrawRay(NormalDoorPivotPoint.position, NormalDoorPivotPoint.forward * 2f);
}
}
}
public class RitualStoneLock : MonoBehaviour
{
public GameObject StoneKey;
public UnityEvent TryOpenDoor;
public bool Completed = false;
private Collider thisObjectsCollider;

private void Start()
{
thisObjectsCollider = GetComponent<Collider>();
}

private void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("canPickUp") && other.gameObject == StoneKey)
{
var temp = other.GetComponentInParent<PickupAndThrow>();
if (temp != null)
{
temp.DropObject();
}

if (other.TryGetComponent<Rigidbody>(out Rigidbody tempRB))
{
tempRB.isKinematic = true;
}
other.gameObject.tag = "Untagged";
other.transform.parent = transform;
other.transform.localPosition = Vector3.zero;
other.transform.localRotation = Quaternion.identity;
Completed = true;
TryOpenDoor.Invoke();
FMODUnity.RuntimeManager.PlayOneShot("event:/MainMenu_Click", GetComponent<Transform>().position);
thisObjectsCollider.enabled = false;
}
}
}

My Takeaways

Some miscommunication issues during the project.

Making compelling AI is difficult.

Optimizing for mobile is difficult while trying to keep similar visuals.

Might have overscoped a bit too much. It would have been better to use the extra time to polish.