Custom species
A custom species is a playable creature that the game does not have. You bring a model and its animations, set values and attacks in the Server Tool, and players pick it on your server. Under the hood the player controls a host species from the game, for example a Deinosuchus, and the custom model is drawn on top of it. Movement, swimming, hunger, growth, saving and the game's own rules come from the host. The values, the attacks and the look come from you.
The species reference lists every field. This guide follows the flow in the Server Tool and then the Lua side.
What you need
- A skeletal model as FBX with its textures.
- Animations as FBX files, one animation per file, on the same skeleton.
- Unreal Engine 5.6 installed on the machine that runs the Server Tool. The tool finds it by itself and tells you when it is missing.
- A host species whose body fits the model. A swimming creature belongs on a swimming host, because the collision and the movement stay those of the host.
The Server Tool, tab Species
The species are listed on the left. The editor on the right has the sections Import, Look, Animations, Sounds, Stats, Habitat, Attacks, Access and Build.
- Import. Pick the folder with the model, the textures and the animation files. The tool copies it into
species\<name>\source, reads every file and shows the model data and the animation list. - Look. Scale, offset and yaw of the model on the host. Turn the model here when it faces the wrong way.
hideHoststays on, the host model is invisible but keeps moving. - Animations. Assign a role to each animation. The tool suggests roles from the file names. Roles you leave empty fall back as the reference describes, so a species with
idle,swimanddeathalready works. The blend space editor arranges animations on a grid of turning and pitch, so a swimmer bends into its turns and dives. - Sounds. Add sound files and place them on the timeline of an animation, see Sounds below.
- Stats. Start from the host. The tool fills in the real host values, and you change what should differ, as a fixed value or as a factor.
- Habitat. Where the species may be, and how much it suffers outside.
- Attacks. Each attack has a key, an animation, damage, stamina cost, cooldown and a hit shape. The small top view shows the cone or the sphere. An attack with the source
host:<ability>keeps the host's own attack and only changes its animation. - Access. A permission and a player limit, or nothing for everyone.
- Build. Runs the pipeline: import into Unreal, cook, pack. The log shows the progress. The result lands in
client-mods\species_<name>.pakand reaches players through the normal mod download on their next join.
Save writes species\<name>\species.json. A running server reads it within a few seconds. Values and attacks change right away for every player who has the species. A new model needs a rejoin.
Sounds
Sounds belong to animations. You place a sound at a moment of an animation, and it plays whenever that animation plays: a snap when the jaws close in the bite, a splash at every stroke of the swim cycle, a roar in the call. The sound plays at the animal, fades out with distance and is heard by every player nearby.
The sound list. Add sound files adds WAV or OGG files, up to 20 MB each. The tool copies them into species\<name>\source\Sounds. Every sound has:
| Field | Meaning |
|---|---|
| Id | The name the timeline uses. a-z, 0-9 and _. |
| Kind | Voice for calls, roars and growls, Body for bites, splashes and steps. Voice follows the game's creature voice volume, Body its creature sound volume. |
| Heard up to | The distance in meters where the sound has faded out. Around 40 for close sounds, 120 for a roar, several hundred for a call that carries across the map. |
The play button next to a sound plays the file in the tool.
The timeline. Pick an animation. The hint below the list shows where it plays, for example Swim or attack bite. Show the animation loads the model with this animation, so you see the pose at every moment. Click or drag in the timeline to move the playhead. The model follows. Choose a sound and press Add at playhead. Play runs the animation in a loop with all its sounds, the way players will hear it.
Click a marker to edit it:
| Field | Meaning |
|---|---|
| Sound | Which sound of the list plays. |
| Time | Seconds from the start of the animation. Drag the marker to move it. |
| Volume | 1 is the file as it is. |
| Pitch | Above 1 is higher and shorter, below 1 deeper and longer. |
| Attach to bone | Optional. The sound moves with this bone, for example the head for a bite. Empty uses the whole body. |
The sounds are built into the animations. They stay in time when an animation plays faster or slower through its speed setting, and a looping animation plays its sounds on every loop. In a direction grid, the sounds of the middle cell (straight ahead, level) also play in every other cell of the grid that has no sounds of its own, moved to the same point of its cycle. So one set of splashes on the middle swim animation covers all turns and dives.
Sounds are part of the pak, so adding or moving them needs a new build. Players get them with the pak on their next join.
Giving a species to a player
Every player can be given a species from a script. A simple chat command:
modkit.commands.add("species", function(player, name)
if not name then
player:message("Species: " .. table.concat(modkit.species.list(), ", ") .. ". Use /species <name> or /species none.")
return
end
local ok, why = player:setCustomSpecies(name ~= "none" and name or nil)
if ok then
player:message(name == "none" and "You are back to normal." or ("You are now a " .. modkit.species.get(name).label .. "."))
else
player:message(why)
end
end)
setCustomSpecies turns the player into the host species first when they play something else, which spawns a fresh dinosaur. The custom values apply on that spawn and on every later spawn as the host. The assignment survives restarts and rides along in the garage.
Access rules apply to this call: a species with a permission needs the player to have it, and a full species is refused. Pass true as the second argument for an admin command that ignores both.
A picker
A small server side picker that shows what is available and applies the choice. Players type /pick and get a numbered list, then /pick 2.
local function available(player)
local names = {}
for _, name in ipairs(modkit.species.list()) do
local def = modkit.species.get(name)
if def.enabled and (def.access.permission == "" or player:hasPermission(def.access.permission)) then
names[#names + 1] = name
end
end
return names
end
modkit.commands.add("pick", function(player, choice)
local names = available(player)
if #names == 0 then return player:message("No species for you on this server.") end
local index = tonumber(choice)
if not index then
for i, name in ipairs(names) do
local def = modkit.species.get(name)
player:message(i .. ". " .. def.label .. " (" .. #modkit.species.players(name) .. " playing)")
end
return player:message("Type /pick <number>. /pick 0 removes your species.")
end
if index == 0 then player:setCustomSpecies(nil) return player:message("Species removed.") end
local name = names[index]
if not name then return player:message("No such entry.") end
local ok, why = player:setCustomSpecies(name)
player:message(ok and ("You are now a " .. modkit.species.get(name).label .. ".") or why)
end)
The same list can feed an NUI page: send the names and labels to the client with player:call, let the page show cards, and send the choice back with callRemote. The server handler then calls setCustomSpecies like above.
Reacting to attacks
modkit.events.add("customAttack", function(player, attackId)
if player.growth < 0.5 and attackId == "tail_slam" then
player:message("You are too young for that.")
return false
end
end)
modkit.events.add("customAttackHit", function(player, attackId, victim, damage)
if victim.steamId then
modkit.log.info(player.name .. " hit " .. victim.name .. " with " .. attackId .. " for " .. damage)
end
end)
modkit.events.add("customSpeciesChanged", function(player, from, to)
if to then modkit.chat.broadcast(player.name .. " is now a " .. modkit.species.get(to).label) end
end)
A species without the tool
modkit.species.define creates a species from a script. The model must already be built and lie in client-mods under the species name. This is useful for variants of one model: the same Mosasaurus as a faster, weaker event version.
modkit.events.add("serverStarted", function()
local base = modkit.species.get("mosasaurus")
if not base then return end
modkit.species.define("mosasaurus_event", {
label = "Event Mosasaurus",
host = base.host,
visual = base.visual,
animations = base.animations,
thresholds = base.thresholds,
stats = { health = { factor = 0.6 }, swimSpeed = { factor = 1.4 } },
habitat = base.habitat,
attacks = base.attacks,
access = { permission = "event.player" },
})
end)
The model is looked up by the name, so mosasaurus_event needs its own pak with assets named after it. For a variant that reuses the Mosasaurus pak, keep the name and change the values with define("mosasaurus", ...) instead. That replaces the file definition while the server runs.
Things to know
- Every player runs the client mod, so everyone sees the custom model. There is no fallback look.
- A missing pak or a missing asset leaves the host model visible for that species. The client log names the missing path.
- The hit test runs on the server. Clients only play the animation the server tells them to.
damageTakencorrects the health after the game applied a hit. A single hit that takes all remaining health still kills.growthSecondsuses the growth control.player:setGrowthMultiplierandplayer:freezeGrowthoverride it for that player.- Custom attacks hit players and animals with a handle. Wild animals without a handle are not hit by custom attacks, only by the host's own attacks.