Role: Programmer, Game Designer
Tool: Unity, Mixamo
Team: Ludum Dare 56 (Tiny Creatures), Team of 6
Timeline: 72 hours
Completion Date: October 7, 2024
Role: Programmer, Game Designer
Tool: Unity, Mixamo
Team: Ludum Dare 56 (Tiny Creatures), Team of 6
Timeline: 72 hours
Completion Date: October 7, 2024
Role: Programmer, Game Designer
Tool: Unity, Mixamo
Team: Ludum Dare 56 (Tiny Creatures), Team of 6
Timeline: 72 hours
Completion Date: October 7, 2024
Play ->
Play ->
Play ->
GAME DESIGN
GAME DESIGN
GAME DESIGN
design pillars
design pillars
design pillars
Resource Management
Resource Management
Resource Management
Focus on strategic stealth gameplay to acquire polygons through careful, undetected actions.
Focus on strategic stealth gameplay to acquire polygons through careful, undetected actions.
Focus on strategic stealth gameplay to acquire polygons through careful, undetected actions.
Dynamic Enemy AI
Dynamic Enemy AI
Dynamic Enemy AI
Develop responsive enemy behavior that challenges players to adapt their strategies.
Develop responsive enemy behavior that challenges players to adapt their strategies.
Develop responsive enemy behavior that challenges players to adapt their strategies.
Tension Through Progression
Tension Through Progression
Tension Through Progression
Create a high-stakes dynamic where growth depends on resource management and critical decision-making.
Create a high-stakes dynamic where growth depends on resource management and critical decision-making.
Create a high-stakes dynamic where growth depends on resource management and critical decision-making.
mechanics
mechanics
mechanics
PROGRAMMING
PROGRAMMING
PROGRAMMING
Enemy AI System
Enemy AI System
Enemy AI System
The AI system consists of two main components: sensors and actions. Sensors are divided into vision and hearing.
Vision involves three simultaneous checks. First, verify if the player is within the enemy’s field of view by comparing the angle between their directional vectors. We then cast a ray towards the player to ensure no obstructions block visibility, and only after that do we measure the distance to the player to check if the player is in range.
Hearing is simpler, detecting the player’s movement state (running, walking, or crouching). If the player runs or walks within a certain radius, the enemy "hears" it.
Below are key functions from EnemyAI.cs
for vision and hearing implementation:
The AI system consists of two main components: sensors and actions. Sensors are divided into vision and hearing.
Vision involves three simultaneous checks. First, verify if the player is within the enemy’s field of view by comparing the angle between their directional vectors. We then cast a ray towards the player to ensure no obstructions block visibility, and only after that do we measure the distance to the player to check if the player is in range.
Hearing is simpler, detecting the player’s movement state (running, walking, or crouching). If the player runs or walks within a certain radius, the enemy "hears" it.
Below are key functions from EnemyAI.cs
for vision and hearing implementation:
The AI system consists of two main components: sensors and actions. Sensors are divided into vision and hearing.
Vision involves three simultaneous checks. First, verify if the player is within the enemy’s field of view by comparing the angle between their directional vectors. We then cast a ray towards the player to ensure no obstructions block visibility, and only after that do we measure the distance to the player to check if the player is in range.
Hearing is simpler, detecting the player’s movement state (running, walking, or crouching). If the player runs or walks within a certain radius, the enemy "hears" it.
Below are key functions from EnemyAI.cs
for vision and hearing implementation:
float DistanceToPlayer()
{
return Vector3.Distance(transform.position, player.transform.position);
}
bool PlayerIsRunning()
{
return playerMovement.GetMovementState() == PlayerMovement.MovementState.RUN
&& playerMovement.GetMoveVelocity().magnitude > 0;
}
bool PlayerIsWalking()
{
return playerMovement.GetMovementState() == PlayerMovement.MovementState.WALK
&& playerMovement.GetMoveVelocity().magnitude > 0;
}
bool PlayerInFieldOfView()
{
Vector3 directionToPlayer = (player.transform.position - transform.position).normalized;
float angleBetweenEnemyAndPlayer = Vector3.Angle(
player.transform.position.y * transform.up + transform.forward,
directionToPlayer);
return angleBetweenEnemyAndPlayer <= fovAngle / 2f;
}
bool PlayerVisible()
{
Vector3 directionToPlayer = (player.transform.position - transform.position).normalized;
if (Physics.Raycast(transform.position, directionToPlayer, out RaycastHit hit, sightRange, ~LayerMask.GetMask("Enemy")))
{
if (hit.transform.root.gameObject == player.transform.gameObject)
{
return true;
}
return false;
}
return false;
}
bool PlayerDetected()
{
if ((PlayerInFieldOfView() && PlayerVisible() && DistanceToPlayer() <= sightRange)
|| (PlayerIsRunning() && DistanceToPlayer() <= listenRange)
|| (PlayerIsWalking() && DistanceToPlayer() <= listenRange * 0.5f)
)
{
detectedBefore = true;
lastDetectedTime = Time.time;
return true;
}
return false
float DistanceToPlayer()
{
return Vector3.Distance(transform.position, player.transform.position);
}
bool PlayerIsRunning()
{
return playerMovement.GetMovementState() == PlayerMovement.MovementState.RUN
&& playerMovement.GetMoveVelocity().magnitude > 0;
}
bool PlayerIsWalking()
{
return playerMovement.GetMovementState() == PlayerMovement.MovementState.WALK
&& playerMovement.GetMoveVelocity().magnitude > 0;
}
bool PlayerInFieldOfView()
{
Vector3 directionToPlayer = (player.transform.position - transform.position).normalized;
float angleBetweenEnemyAndPlayer = Vector3.Angle(
player.transform.position.y * transform.up + transform.forward,
directionToPlayer);
return angleBetweenEnemyAndPlayer <= fovAngle / 2f;
}
bool PlayerVisible()
{
Vector3 directionToPlayer = (player.transform.position - transform.position).normalized;
if (Physics.Raycast(transform.position, directionToPlayer, out RaycastHit hit, sightRange, ~LayerMask.GetMask("Enemy")))
{
if (hit.transform.root.gameObject == player.transform.gameObject)
{
return true;
}
return false;
}
return false;
}
bool PlayerDetected()
{
if ((PlayerInFieldOfView() && PlayerVisible() && DistanceToPlayer() <= sightRange)
|| (PlayerIsRunning() && DistanceToPlayer() <= listenRange)
|| (PlayerIsWalking() && DistanceToPlayer() <= listenRange * 0.5f)
)
{
detectedBefore = true;
lastDetectedTime = Time.time;
return true;
}
return false
With the sensors in place, enemies can detect the player and respond accordingly. The AI operates using a simplified Hierarchical FSM, divided into two parent states: passive and aggressive. In the aggressive state, triggered when the player is detected, enemies choose to chase or attack based on proximity. In the passive state, enemies either patrol or remain idle, depending on whether patrol points are assigned.
While the AI system could be expanded with more states and modular classes for each state, this implementation meets the scope and constraints of the game jam, balancing functionality and development efficiency.
Below is a code snippet from EnemyAI.cs
showcasing this streamlined Finite State Machine for enemy movement:
With the sensors in place, enemies can detect the player and respond accordingly. The AI operates using a simplified Hierarchical FSM, divided into two parent states: passive and aggressive. In the aggressive state, triggered when the player is detected, enemies choose to chase or attack based on proximity. In the passive state, enemies either patrol or remain idle, depending on whether patrol points are assigned.
While the AI system could be expanded with more states and modular classes for each state, this implementation meets the scope and constraints of the game jam, balancing functionality and development efficiency.
Below is a code snippet from EnemyAI.cs
showcasing this streamlined Finite State Machine for enemy movement:
With the sensors in place, enemies can detect the player and respond accordingly. The AI operates using a simplified Hierarchical FSM, divided into two parent states: passive and aggressive. In the aggressive state, triggered when the player is detected, enemies choose to chase or attack based on proximity. In the passive state, enemies either patrol or remain idle, depending on whether patrol points are assigned.
While the AI system could be expanded with more states and modular classes for each state, this implementation meets the scope and constraints of the game jam, balancing functionality and development efficiency.
Below is a code snippet from EnemyAI.cs
showcasing this streamlined Finite State Machine for enemy movement:
if (PlayerDetected() || (detectedBefore && Time.time - lastDetectedTime < maxDetectTime) || wasStolen)
{
UISteal.SetActive(false);
if (DistanceToPlayer() <= attackRange)
{
SetCurrentState(ActionState.ATTACK);
TurnToPlayer();
}
else
{
SetCurrentState(ActionState.CHASE);
agent.speed = chaseSpeed;
}
UIExclamationMark.SetActive(true);
agent.SetDestination(player.transform.position);
}
// Player not detected
else
{
// Patrol
if (walkpoints.Count > 1)
{
SetCurrentState(ActionState.PATROL);
agent.speed = patrolSpeed;
if (detectedBefore && Time.time - lastDetectedTime > maxDetectTime)
{
FindNearestPatrolPoint();
detectedBefore = false;
}
if (Vector3.Distance(transform.position, walkpoints[currentWalkpointIndex].position) < 1f)
{
currentWalkpointIndex = (currentWalkpointIndex + 1) % walkpoints.Count;
}
targetPosition = walkpoints[currentWalkpointIndex].position;
}
// Idle
else
{
SetCurrentState(ActionState.IDLE);
targetPosition = transform.position;
}
agent.SetDestination(targetPosition);
UIExclamationMark.SetActive(false
if (PlayerDetected() || (detectedBefore && Time.time - lastDetectedTime < maxDetectTime) || wasStolen)
{
UISteal.SetActive(false);
if (DistanceToPlayer() <= attackRange)
{
SetCurrentState(ActionState.ATTACK);
TurnToPlayer();
}
else
{
SetCurrentState(ActionState.CHASE);
agent.speed = chaseSpeed;
}
UIExclamationMark.SetActive(true);
agent.SetDestination(player.transform.position);
}
// Player not detected
else
{
// Patrol
if (walkpoints.Count > 1)
{
SetCurrentState(ActionState.PATROL);
agent.speed = patrolSpeed;
if (detectedBefore && Time.time - lastDetectedTime > maxDetectTime)
{
FindNearestPatrolPoint();
detectedBefore = false;
}
if (Vector3.Distance(transform.position, walkpoints[currentWalkpointIndex].position) < 1f)
{
currentWalkpointIndex = (currentWalkpointIndex + 1) % walkpoints.Count;
}
targetPosition = walkpoints[currentWalkpointIndex].position;
}
// Idle
else
{
SetCurrentState(ActionState.IDLE);
targetPosition = transform.position;
}
agent.SetDestination(targetPosition);
UIExclamationMark.SetActive(false
Leveling System
Leveling System
Leveling System
Leveling up or down reflects the player's progress, driven by gaining or losing polygons. The objective is to accumulate polygons until reaching the maximum capacity at level 3, showcasing mastery of stealth and parry mechanics.
The GainPoly()
and LosePoly()
functions adjust the player's polygon count, triggering LevelUp()
or LevelDown()
if the count exceeds the current level's limits. These functions update the player's level and appearance using UpgradeModelAndRig()
or DowngradeModelAndRig()
.
The player object stores three mesh and rig variations, each under an inactive parent labeled Level <level_number> Body
. To change appearances, the appropriate mesh and rig are activated by reassigning them directly under the player object and updating the animator controller, with inactive versions returned to their respective bodies.
Below is a snippet from PlayerLevelScript.cs
, dedicated to managing the player's polygon and level stats:
Leveling up or down reflects the player's progress, driven by gaining or losing polygons. The objective is to accumulate polygons until reaching the maximum capacity at level 3, showcasing mastery of stealth and parry mechanics.
The GainPoly()
and LosePoly()
functions adjust the player's polygon count, triggering LevelUp()
or LevelDown()
if the count exceeds the current level's limits. These functions update the player's level and appearance using UpgradeModelAndRig()
or DowngradeModelAndRig()
.
The player object stores three mesh and rig variations, each under an inactive parent labeled Level <level_number> Body
. To change appearances, the appropriate mesh and rig are activated by reassigning them directly under the player object and updating the animator controller, with inactive versions returned to their respective bodies.
Below is a snippet from PlayerLevelScript.cs
, dedicated to managing the player's polygon and level stats:
Leveling up or down reflects the player's progress, driven by gaining or losing polygons. The objective is to accumulate polygons until reaching the maximum capacity at level 3, showcasing mastery of stealth and parry mechanics.
The GainPoly()
and LosePoly()
functions adjust the player's polygon count, triggering LevelUp()
or LevelDown()
if the count exceeds the current level's limits. These functions update the player's level and appearance using UpgradeModelAndRig()
or DowngradeModelAndRig()
.
The player object stores three mesh and rig variations, each under an inactive parent labeled Level <level_number> Body
. To change appearances, the appropriate mesh and rig are activated by reassigning them directly under the player object and updating the animator controller, with inactive versions returned to their respective bodies.
Below is a snippet from PlayerLevelScript.cs
, dedicated to managing the player's polygon and level stats:
public void GainPoly(int polyNumber)
{
if (currentPoly + polyNumber > polyRequiredToNextLevel)
{
LevelUp(polyNumber);
}
else {
currentPoly += polyNumber;
}
UpdateProgressBar();
}
public void LosePoly(int polyNumber)
{
if (currentPoly - polyNumber < 0)
{
LevelDown(polyNumber);
}
else {
currentPoly -= polyNumber;
}
UpdateProgressBar();
}
private void LevelUp(int polyNumber)
{
if (currentLevel + 1 > maxLevel)
{
ShowWin();
return;
}
currentLevel++;
UpgradeModelAndRig();
currentPoly += polyNumber - polyRequiredToNextLevel;
polyRequiredToNextLevel = (int) (polyRequiredToNextLevel * levelMultiplier);
}
private void LevelDown(int polyNumber)
{
if (currentLevel - 1 < 1) return;
currentLevel--;
DowngradeModelAndRig();
polyRequiredToNextLevel = (int) (polyRequiredToNextLevel / levelMultiplier);
currentPoly += polyRequiredToNextLevel - polyNumber;
}
private void UpgradeModelAndRig()
{
transform.Find("Model").SetParent(transform.Find("Level " + (currentLevel - 1) + " Body"));
transform.Find("mixamorig:Hips").SetParent(transform.Find("Level " + (currentLevel - 1) + " Body"));
int currentIndex = currentLevel - 1;
bodies[currentIndex].transform.Find("Model").SetParent(transform);
bodies[currentIndex].transform.Find("mixamorig:Hips").SetParent(transform);
GetComponent<Animator>().runtimeAnimatorController = animatorControllers[currentIndex];
}
private void DowngradeModelAndRig()
{
transform.Find("Model").SetParent(transform.Find("Level " + (currentLevel + 1) + " Body"));
transform.Find("mixamorig:Hips").SetParent(transform.Find("Level " + (currentLevel + 1) + " Body"));
int currentIndex = currentLevel - 1;
bodies[currentIndex].transform.Find("Model").SetParent(transform);
bodies[currentIndex].transform.Find("mixamorig:Hips").SetParent(transform);
GetComponent<Animator>().runtimeAnimatorController = animatorControllers[currentIndex
public void GainPoly(int polyNumber)
{
if (currentPoly + polyNumber > polyRequiredToNextLevel)
{
LevelUp(polyNumber);
}
else {
currentPoly += polyNumber;
}
UpdateProgressBar();
}
public void LosePoly(int polyNumber)
{
if (currentPoly - polyNumber < 0)
{
LevelDown(polyNumber);
}
else {
currentPoly -= polyNumber;
}
UpdateProgressBar();
}
private void LevelUp(int polyNumber)
{
if (currentLevel + 1 > maxLevel)
{
ShowWin();
return;
}
currentLevel++;
UpgradeModelAndRig();
currentPoly += polyNumber - polyRequiredToNextLevel;
polyRequiredToNextLevel = (int) (polyRequiredToNextLevel * levelMultiplier);
}
private void LevelDown(int polyNumber)
{
if (currentLevel - 1 < 1) return;
currentLevel--;
DowngradeModelAndRig();
polyRequiredToNextLevel = (int) (polyRequiredToNextLevel / levelMultiplier);
currentPoly += polyRequiredToNextLevel - polyNumber;
}
private void UpgradeModelAndRig()
{
transform.Find("Model").SetParent(transform.Find("Level " + (currentLevel - 1) + " Body"));
transform.Find("mixamorig:Hips").SetParent(transform.Find("Level " + (currentLevel - 1) + " Body"));
int currentIndex = currentLevel - 1;
bodies[currentIndex].transform.Find("Model").SetParent(transform);
bodies[currentIndex].transform.Find("mixamorig:Hips").SetParent(transform);
GetComponent<Animator>().runtimeAnimatorController = animatorControllers[currentIndex];
}
private void DowngradeModelAndRig()
{
transform.Find("Model").SetParent(transform.Find("Level " + (currentLevel + 1) + " Body"));
transform.Find("mixamorig:Hips").SetParent(transform.Find("Level " + (currentLevel + 1) + " Body"));
int currentIndex = currentLevel - 1;
bodies[currentIndex].transform.Find("Model").SetParent(transform);
bodies[currentIndex].transform.Find("mixamorig:Hips").SetParent(transform);
GetComponent<Animator>().runtimeAnimatorController = animatorControllers[currentIndex