Preface
Voidwalkers is a round-based survival game where players face waves of zombies (Voidwalkers) that grow stronger and faster each round. The goal is to survive as long as possible by using the environment, upgrading gear, or finding and completing the main story. For those who enjoy exploring, there are multiple Easter eggs and plenty of options for different high-round strategies. Check out the commentated playthrough below or play it for yourself here!
Gameplay Video
Details
System Design
The two most important loops in this game are the core gameplay loop and the zombie behavior loop.
Code Snippets
# Define zombie behavior class
zombie_behavior := class(npc_behavior):
# Access my Game Manager creative device
@editable
GameManager : game_manager = game_manager{}
# Override function that begins the behavior by starting pathing on initialization
OnBegin(): void =
StartPathing()
# Start pathfinding behavior for the zombie
StartPathing(): void =
if:
# Retrieve zombie's character agent, its Navigatable and instantiate
# a variable for the closest player
Agent := GetAgent[],
Character := Agent.GetFortCharacter[],
Navigatable := Character.GetNavigatable[],
var ClosestPlayer : agent = Agent
then:
# Loop to continuously update zombie's target and actions
loop:
# Use the game manager to determine the closest target player
set ClosestPlayer = GameManager.GetClosestTarget(Agent)
# If a valid target is found and pathing is allowed
if (ClosestPlayer <> Agent):
if (GameManager.ShouldPath = true):
# Set navigation target to closest player
NavTarget := MakeNavigationTarget(ClosestPlayer)
NavResultGoTo := Navigatable.NavigateTo(NavTarget)
# If the zombie reaches the target, trigger attack animation and sound
if (Anim := Character.GetPlayAnimationController[]):
if (NavResultGoTo = navigation_result.Reached):
Anim.Play(Mixamo.AttackAnim) # Play attack animation
GameManager.PlayZombieSound(Agent) # Play zombie sound
Sleep(0.7) # Short delay for animation timing
GameManager.DealDamage(ClosestPlayer) # Deal damage
else:
# Stop zombie's navigation if no valid target
Navigatable.StopNavigation()
Sleep(0.5) # Loop delay to prevent the game from crashing
#Player map to track the players' activity status
var PlayerMap : [agent]logic = map{}
#Zombie map to to store all active zombies
var ZombieMap : [agent]int = map{}
#Zombie stats
var ZombieHealth : float = 30.0
var ZombieDamage : float = 25.0
var BaseZombieSpeed : float = 0.6
var MaxZombieSpeed : float = 0.8
var ZombieSpeedCap : float = 1.1
#Zombie spawners
var SpawnerNumber : int = 0
var TotalSpawners : int = 1
#Round Settings
var MaxZombiesPerRound : int = 10
var ZombiesSpawnedThisRound : int = 0
var RoundNumber : int = 1
#Zombies alive at the same time
var MaxActiveZombies : int = 50
var ActiveZombies : int = 0;
#Zombie round buffs
var ZombieIncreasePerRound : int = 5
var ZombieSpeedIncreasePerRound : float = 0.05
#User Interface
var UIPerAgent : [agent]?tuple(canvas,text_block, text_block, text_block, text_block) = map{}
#Pathing logic to avoid post mortem pathing and damage
var ShouldPath : logic = true
#Score
var TotalElims : int = 0
# Ends the round by buffing zombies and setting up parameters for the next round
EndRound() : void =
# Play sound to signal the end of the round
RoundEndAudioPlayer.Play()
# Apply buffs to zombie stats for the next round
BuffZombies()
# Increment the round number and reset zombie counters
set RoundNumber += 1
set ActiveZombies = 0
set ZombiesSpawnedThisRound = 0
# Get the collection of teams in the game
TeamCollection := GetPlayspace().GetTeamCollection()
Teams := GetPlayspace().GetTeamCollection().GetTeams()
# Set maximum number of zombies for the next round based on team size
if (PlayerTeam := Teams[0], TeamMembers := TeamCollection.GetAgents[PlayerTeam]):
set MaxZombiesPerRound += ZombieIncreasePerRound * TeamMembers.Length
# Update the UI for each team member with new round stats
for (TeamMember : TeamMembers):
UpdateUI(TeamMember)
# Start the timer for the next round
TimerToNextRound.Start()
# Buffs the last few zombies at the end of a round to increase their speed slightly
BuffLastZombies() : void=
for (Zombie->Value : ZombieMap):
if (ZombieCharacter := Zombie.GetFortCharacter[], ZombieNavigatable := ZombieCharacter.GetNavigatable[]):
# Increase movement speed of each remaining zombie slightly
ZombieNavigatable.SetMovementSpeedMultiplier(1.05)
# Increase zombie base stats, including health and speed, at the start of each new round
BuffZombies() : void=
# Increment zombie health slightly for the new round
set ZombieHealth += 5.0
# Increase zombie speed if it hasn't reached the speed cap
if (MaxZombieSpeed < ZombieSpeedCap):
set BaseZombieSpeed += ZombieSpeedIncreasePerRound
set MaxZombieSpeed += ZombieSpeedIncreasePerRound
# Get the closest player to a specified zombie and return that player
GetClosestTarget(Zombie : agent) : agent =
# Initialize an array to store distances and a map to associate distances with players
var MagnitudeArray : []float = array{}
var ZPDistancesMap : [float]agent = map{}
# Retrieve the collection of teams within the playspace
TeamCollection := GetPlayspace().GetTeamCollection()
Teams := GetPlayspace().GetTeamCollection().GetTeams()
# Check if the first team in the list is the player team and get its members
# The reason for this way of getting the players and not using GetPlayers() is so that I can test with AI
if (PlayerTeam := Teams[0], TeamMembers := TeamCollection.GetAgents[PlayerTeam]):
# Calculate distances (magnitudes) between the zombie and each player character, storing each in ZPDistancesMap
for (TeamMember : TeamMembers):
if (PlayerCharacter := TeamMember.GetFortCharacter[], PlayerCharacter.IsActive[]):
if (ThisZombie := Zombie.GetFortCharacter[]):
# Calculate the 3D distance between the zombie and player and store it in ZPDistancesMap
if (set ZPDistancesMap[Sqrt(Pow(PlayerCharacter.GetTransform().Translation.X - ThisZombie.GetTransform().Translation.X, 2.0)
+ Pow(PlayerCharacter.GetTransform().Translation.Y - ThisZombie.GetTransform().Translation.Y, 2.0)
+ Pow(PlayerCharacter.GetTransform().Translation.Z - ThisZombie.GetTransform().Translation.Z, 2.0))] = TeamMember){}
# Transfer distance (magnitude) keys from ZPDistancesMap into MagnitudeArray
for (Key->Value : ZPDistancesMap):
NewMagnitude : []float = array{Key}
set MagnitudeArray = MagnitudeArray + NewMagnitude
# Sort MagnitudeArray in ascending order so the lowest distance is at index 0
for (IDX := 0..MagnitudeArray.Length - 1):
if (MagnitudeArray[IDX] > MagnitudeArray[IDX + 1], Temp := MagnitudeArray[IDX], Current := MagnitudeArray[IDX + 1]):
if (set MagnitudeArray[IDX] = Current, set MagnitudeArray[IDX + 1] = Temp){}
# Iterate through sorted distances and return the closest active player
for (Index := 0..MagnitudeArray.Length - 1):
if (ClosestPlayer := ZPDistancesMap[MagnitudeArray[Index]]):
if (ClosestPlayerCharacter := ClosestPlayer.GetFortCharacter[], ClosestPlayerCharacter.IsActive[]):
return ClosestPlayer # Return the closest active player
# Return the zombie itself if no players are found
return Zombie
Map Design
Below, you'll find details about the map and its specific sections. Please note that some details may vary in future versions of the game.