Preface
The Colosseum is a round-based survival game where players face waves of creatures that grow stronger and faster each round. The goal is to survive as long as possible by using the environment, unlocking gear, or the defeating the champions of the arena. For those who enjoy exploring, there are multiple Easter eggs and plenty of options for different high-round strategies. Check out the trailer below or play it for yourself here!
Trailer
Details
System Design
The two most important loops in this game are the core gameplay loop and the creature behavior loop.
Code Snippets
creature_behavior := class(npc_behavior):
# Access my Game Manager creative device to access functions
@editable
GameManager : game_manager = game_manager{}
# This function begins the behavior by starting pathing on initialization of the game
OnBegin(): void =
StartPathing()
# Start pathfinding behavior for the creature
StartPathing(): void =
if:
# Retrieve creature'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 creature'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 creature 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.PlayCreatureSound(Agent) # Play creature sound
Sleep(0.7) # Short delay for animation timing
GameManager.DealDamage(ClosestPlayer) # Deal damage
else:
# Stop creature'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{}
#Creature map to to store all active creatures
var CreatureMap : [agent]int = map{}
#Creature stats
var CreatureHealth : float = 30.0
var CreatureDamage : float = 25.0
var BaseCreatureSpeed : float = 0.6
var MaxCreatureSpeed : float = 0.8
var CreatureSpeedCap : float = 1.1
#Creature spawners
var SpawnerNumber : int = 0
var TotalSpawners : int = 1
#Round Settings
var MaxCreaturesPerRound : int = 10
var CreaturesSpawnedThisRound : int = 0
var RoundNumber : int = 1
#Creatures alive at the same time
var MaxActiveCreatures : int = 50
var ActiveCreatures : int = 0;
#Creature round buffs
var CreatureIncreasePerRound : int = 5
var CreatureSpeedIncreasePerRound : 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 creatures and setting up parameters for the next round
EndRound() : void =
# Play sound to signal the end of the round
RoundEndAudioPlayer.Play()
# Apply buffs to creature stats for the next round
BuffCreatures()
# Increment the round number and reset creature counters
set RoundNumber += 1
set ActiveCreatures = 0
set CreaturesSpawnedThisRound = 0
# Get the collection of teams in the game
TeamCollection := GetPlayspace().GetTeamCollection()
Teams := GetPlayspace().GetTeamCollection().GetTeams()
# Set maximum number of creatures for the next round based on team size
if (PlayerTeam := Teams[0], TeamMembers := TeamCollection.GetAgents[PlayerTeam]):
set MaxCreaturesPerRound += CreatureIncreasePerRound * 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 creatures at the end of a round to increase their speed slightly
BuffLastCreatures() : void=
for (Creature->Value : CreatureMap):
if (CreatureCharacter := Creature.GetFortCharacter[], CreatureNavigatable := CreatureCharacter.GetNavigatable[]):
# Increase movement speed of each remaining creature slightly
CreatureNavigatable.SetMovementSpeedMultiplier(1.05)
# Increase creature base stats, including health and speed, at the start of each new round
BuffCreatures() : void=
# Increment creature health slightly for the new round
set CreatureHealth += 5.0
# Increase creature speed if it hasn't reached the speed cap
if (MaxCreatureSpeed < CreatureSpeedCap):
set BaseCreatureSpeed += CreatureSpeedIncreasePerRound
set MaxCreatureSpeed += CreatureSpeedIncreasePerRound
# Get the closest player to a specified creature and return that player
GetClosestTarget(Creature : 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 creature and each player character, storing each in ZPDistancesMap
for (TeamMember : TeamMembers):
if (PlayerCharacter := TeamMember.GetFortCharacter[], PlayerCharacter.IsActive[]):
if (ThisCreature := Creature.GetFortCharacter[]):
# Calculate the 3D distance between the creature and player and store it in ZPDistancesMap
if (set ZPDistancesMap[Sqrt(Pow(PlayerCharacter.GetTransform().Translation.X - ThisCreature.GetTransform().Translation.X, 2.0)
+ Pow(PlayerCharacter.GetTransform().Translation.Y - ThisCreature.GetTransform().Translation.Y, 2.0)
+ Pow(PlayerCharacter.GetTransform().Translation.Z - ThisCreature.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 creature itself if no players are found
return Creature
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.
That's it! This was such a fun project to work on. It was partly inspired by the Call of Duty: Black Ops 4 Zombies map, IX. It's one of my favorite Zombies maps of all time, and I wanted to create something that captured a similar vibe while offering a unique experience. It took me around two weeks to make and throughout the process, I learned a lot about programming AI in Verse, designing exciting rewards and challenges for players, and integrating fiction with mechanics to create a cohesive experience. I'm happy I got to share this with a lot of Fortnite players, and I hope you enjoyed it as much as I enjoyed creating it!