Jump to content

[Guide] Creating a mob from scratch.


-LukaS-
 Share

Recommended Posts

cool_mob.gif.59bd9fe8b94e646a398bcf903324dd74.gif

Hello and hi!

I haven't seen a lot of tutorials on the subject of creating custom mobs, outside of some really old mod-tutorials by Cheerio (I think), like explaining brains, behaviours in-depth. So, here I go, making this tutorial for beginners trying to create their own mob.

First of all I would like to say that creating an entirely unique mob takes time, and by unique I mean a mob with unique mechanics, attack patterns, behaviours or anything that isn't just walking up to the player and trying to chomp them. It requires you to create a more complex brain, possibly custom components or behaviours, and generally a lot of planning and organizing.
In this tutorial I'll be focusing on a simple mob, without any special qualities. Worry not however as this does not mean that this tutorial won't cover much. There is a lot to cover.

 

WarningThis guide is for modding Don't Starve Together specifically so there is a chance some of the information provided here does not apply to singleplayer Don't Starve.

 

1. Before we start.

Spoiler

Even though I'm making this tutorial very beginner friendly, basic programming skills and Lua knowledge is needed.

Every mob, or any entity really, in Don't Starve Together is compiled of several different files. A prefab, a brain, a stategraph, behaviours and components. The most important file is the prefab file as it's there where you put all of the data you need for mob to exist. And since the prefab file is the most important I'd say we can start with that...

2. What is a prefab?

Spoiler

A prefab is a collection of data that an entity is created from. A prefab is not the same as an entity. Think of a prefab as a template that the game uses to create entities. There isn't really much more that needs to be said but if you want a more detailed explanation check this thread by Hornete:

[Documentation] Understanding Prefabs

 

Prefab files are placed in scripts/prefabs/ and usually contain only 1 prefab however you can write multiple prefabs inside a single file (spider.lua, for example, has 8 spider prefabs at once). A prefab file has to always return at least 1 Prefab class object.

Here's how the Prefab class looks like:

Spoiler
Prefab = Class( function(self, name, fn, assets, deps, force_path_search)
    self.name = string.sub(name, string.find(name, "[^/]*$"))  --remove any legacy path on the name
    self.desc = ""
    self.fn = fn
    self.assets = assets or {}
    self.deps = deps or {}
    self.force_path_search = force_path_search or false
 
    if PREFAB_SKINS[self.name] ~= nil then
        for _,prefab_skin in pairs(PREFAB_SKINS[self.name]) do
            table.insert( self.deps, prefab_skin )
        end
    end
end)
 

If you're not entirely sure what you're looking at right now, that's ok, for now all you need to know are the parameters that this class takes. As you can see there's 5:
name - this is the name of your prefab
fn - this is the function that's run when your mob is created
assets - this is a table of assets your mob uses
deps - this is a table of other prefabs your mob might use
force_path_search - this is a true/false value that's used when searching for assets

The core of every prefab is the 'fn' function as it sets up all the data our mod is composed of. There are many attributes that are required for our mob to work properly, a few of them are engine function meaning unfortunately you cannot look into them and therefor need to guess your way into knowing what they do or what parameters they take. A very simple prefab file would look something like this:

Spoiler
local assets = { -- The assets table for our mob, using spider assets as a template
    Asset("ANIM", "anim/ds_spider_basic.zip"),
    Asset("ANIM", "anim/spider_build.zip"),
    Asset("SOUND", "sound/spider.fsb")
}
 
local function fn()
    local inst = CreateEntity()     -- This function is what creates our entity, we refer to this entity as 'inst' below
                                    -- \/ These are in engine functions used to add the core attributes to our mob
    inst.entity:AddTransform()      -- Transform allows our entity to exist in the world, have a set position, rotation, etc.
    inst.entity:AddAnimState()      -- AnimState allow our entity to exist visually, it also manages animations
    inst.entity:AddSoundEmitter()   -- SoundEmitter allows our entity to create sounds
    inst.entity:AddDynamicShadow()  -- DynamicShadow add a shadow for our entity
    inst.entity:AddNetwork()        -- Network is required for networking purposes
 
    MakeCharacterPhysics(inst, 10, .5)  -- Basic global function that adds the Physics attribute to our entity,
                                        -- allowing it to interact with the world and other entities
    inst.DynamicShadow:SetSize(1.5, 0.5) -- Here we're setting a shadow for our mob
    inst.Transform:SetFourFaced() -- Here we set how many faced/sides our mob has
 
    inst.AnimState:SetBank("spider") -- This method sets our mobs bank file (what animations it has)
    inst.AnimState:SetBuild("spider_build") -- This method sets our mobs build file (what assets it uses)
    inst.AnimState:PlayAnimation("idle") -- This method simply plays the given animation
 
    inst.entity:SetPristine() -- SetPristine tells the game that up to this point we want everything to be set up exactly the same on the server as on the client
 
    if not TheWorld.ismastersim then -- Here we're checking whether we're running on the server or not
        return inst -- If we're on the client we return our entity
    end
   
    -- Code here will only be run on the server
 
    return inst -- At the end we return our entity
end
 
return Prefab("custom_mob", fn, assets) -- And we end the file by returning our prefab with the correct fn and assets

Now you can add your prefab file name to the PrefabFiles table in modmain.lua:

Spoiler
PrefabFiles = {
    "custom_mob"
}

And that's all you need. Using the command c_spawn("custom_mob") you're now able to spawn your custom mob in your world. It doesn't really do much right now. To pump some 'life' into our mob we're gonna need a brain, so...

3. What is a brain?

Spoiler

The brain of a mob, as the name would suggest, is a brain, used by the entity to respond to certain conditions, to make the entity 'think'. Brains are stored in scripts/brains/. In DST brains are composed of behaviour nodes and most brains manage what node should currently influence the mobs action using priority. Here's how a basic brain looks like:

Spoiler
require "behaviours/wander" -- Here we're importing the Wander behaviour
 
local MAX_WANDER_DIST = 32
 
local CMobBrain = Class(Brain, function(self, inst) -- This is our custom mobs brain
    Brain._ctor(self, inst) -- Here we're simply running the base classes constructor
end)
 
function CMobBrain:OnStart() -- Here's where the magic lies
    local root = -- We're declaring a local root, a PriorityNode
        PriorityNode({ -- This priority node has 1 node inside it
            Wander(self.inst, nil, MAX_WANDER_DIST) -- The Wander node makes our mod simply walk around a given point (or not) in the world
        }, 1) -- This is the sleep time between brain checks
       
    self.bt = BT(self.inst, root) -- Here we're declaring the BehaviourTree and we're putting the root inside it
end
 
return CMobBrain -- And return our brain

With the code above our mob will start wandering around and since Wander is the only node present that's all it will be able to do.

But what if I want my mob to be able to do many different things? Well, let's talk about different kind of nodes!

As I said before brains have a priority system for their nodes (you know, because PriorityNode). Putting multiple nodes inside the root sets their priority according to their position in the table. Higher in the table = higher priority. Nodes always have a status and they can be in 1 of 4 different statuses at a time: SUCCESS, FAILED, READY or RUNNING.

SUCCESS is set when the nodes condition are met and the node successfully executed.
FAILED is set when the conditions are not met and so the node is not run.
READY is set if the node is not running and is ready to be checked again. That's because every node goes to sleep after running. Every node starts by checking if the nodes status is READY and if it's not then it checks whether the status is already RUNNING.
RUNNING is set if the nodes conditions are met or the node is currently running.

Now with that information in mind the brain decides what node to execute based on node conditions and their priority. So if we have a brain with a Wander node (which, btw, cannot set it's status to FAILED), and, for example, an AttackWall node above it, if the conditions for AttackWall are met the node will execute, Wander will be skipped. However if AttackWalls conditions are not met, Wander will execute.

For most non-boss mobs the panic nodes, the nodes that manage getting haunted or scared by epic mobs, have the highest priority.

Let's create a bit more complex brain for our mob:

Spoiler
require "behaviours/wander"
require "behaviours/panic"
 
local MAX_WANDER_DIST = 32
local SEE_FOOD_DIST = 16
 
local EATFOOD_CANT_TAGS = { "outofreach" }
local function EatFoodAction(inst)
    local target = FindEntity(inst, -- Here we're trying to find an entity that meets our criteria
        SEE_FOOD_DIST,
        function(item)
            return inst.components.eater:CanEat(item)
                and item:IsOnValidGround()
        end,
        nil,
        EATFOOD_CANT_TAGS
    )
    return target and BufferedAction(inst, target, ACTIONS.EAT) or nil -- If it exists return an EAT action
end
 
local function ShouldPanic()
    return not TheWorld.state.isday -- Return true if it's night or dusk
end
 
local CMobBrain = Class(Brain, function(self, inst)
    Brain._ctor(self, inst)
end)
 
function CMobBrain:OnStart()
    local root =
        PriorityNode({
            WhileNode(function() return ShouldPanic(self.inst) end, "PanicNight", Panic(self.inst)), -- This is the WhileNode,
                                                                                    -- it's run as long as the given condition is met
            DoAction(self.inst, function() return EatFoodAction(self.inst) end),    -- This node will perform an action returned by the given
            Wander(self.inst, nil, MAX_WANDER_DIST)                                 -- will set status to FAILED if there's no action
        }, 1)
       
    self.bt = BT(self.inst, root)
end
 
return CMobBrain

This much will allow our mob to wander around, eat food off the ground and panic when it's night. Now, to give that brain to our mob we put this in the prefab file, right before 'return inst':

Spoiler
inst:SetBrain(require("brains/custom_mobbrain"))

Now, many of the actions we're trying to make our mob do are going to need a certain animation to play, for example, when it tries to eat food off the ground it should play the eating animation. For that there exists a thing called a stategraph, but you might be asking yourself...

4. What is a stategraph?

Spoiler

A stategraph is a huge table of states used by the mob. It is mostly used to play sounds, animations, control the order in which the animations play and respond to some events (through EventHandlers) or actions (through ActionHandlers). States are placed inside scripts/stategraphs/. Most stategraphs are composed of 3 tables:

'actionhandlers' - Contains ActionHandlers which are used to make the entity go to a certain state when performing a certain action, for example, eating food
'events' - Contains EventHandlers which are used to make the entity go to a certain state if a certain event happens, for example, when "attacked" event is pushed our entity should go to a state called "hit"
'states' - Contains states. Everything from idling through attacking or getting attacked to dying.

Lets break down how states look, by adding a few basic ones that our mob will use:

Spoiler
local states = { -- This is our states table
    State{
        name = "idle", -- Our first state will be the idle state
        tags = { "idle", "canrotate" }, -- Here we add tags to our state
 
        onenter = function(inst) -- This function will run when our mob enters this state
            inst.Physics:Stop() -- We make our mob stop moving
            inst.AnimState:PlayAnimation("idle", true) -- And we play the idle animation, 'true' so it loops
        end
    },
 
    State{
        name = "premoving", -- Next we have premoving which it our state that starts our mobs movement
        tags = { "moving", "canrotate" },
 
        onenter = function(inst)
            inst.components.locomotor:WalkForward() -- We make our mob move forward
            inst.AnimState:PlayAnimation("walk_pre") -- And play the appropriate animation
        end,
 
        timeline = { -- Time line is a table of functions that run based on passed frames
            TimeEvent(3*FRAMES, function(inst) inst.SoundEmitter:PlaySound("dontstarve/creatures/spider/walk_spider") end),
        },         -- /\ So after 3 frames after entering this state we play the walking sound
 
        events = { -- Events are a table of EventHandlers that listen for events only during this state
            EventHandler("animover", function(inst) inst.sg:GoToState("moving") end) -- So after our 'walk_pre' animation finishes we go to 'moving'
        }
    },
 
    State{
        name = "moving", -- Moving is a state that has the ability to loop when our mob walks
        tags = { "moving", "canrotate" },
 
        onenter = function(inst)
            inst.components.locomotor:RunForward()
            inst.AnimState:PushAnimation("walk_loop")
        end,
 
        timeline = {
            TimeEvent(0*FRAMES, function(inst) inst.SoundEmitter:PlaySound("dontstarve/creatures/spider/walk_spider") end),
            TimeEvent(3*FRAMES, function(inst) inst.SoundEmitter:PlaySound("dontstarve/creatures/spider/walk_spider") end),
            TimeEvent(7*FRAMES, function(inst) inst.SoundEmitter:PlaySound("dontstarve/creatures/spider/walk_spider") end),
            TimeEvent(12*FRAMES, function(inst) inst.SoundEmitter:PlaySound("dontstarve/creatures/spider/walk_spider") end)
        }, -- Play walking sounds on frame 0, 3, 7 and 12
 
        events = { -- Loop this state after 'walk_loop' finishes
            EventHandler("animover", function(inst) inst.sg:GoToState("moving") end)
        }
    },
 
    State{
        name = "hit", -- This state is for when our mob takes damage
 
        onenter = function(inst)
            inst.AnimState:PlayAnimation("hit")
            inst.Physics:Stop()
        end,
 
        events = { -- After 'hit' go to 'idle'
            EventHandler("animover", function(inst) inst.sg:GoToState("idle") end)
        }
    },
 
    State{
        name = "eat", -- This state is for when our mob tries to eat something
        tags = { "busy" }, -- The tag 'busy' makes this state uninterruptable
 
        onenter = function(inst)
            inst.Physics:Stop()
            inst.AnimState:PlayAnimation("eat")
            inst.SoundEmitter:PlaySound("dontstarve/creatures/spider/eat", "eating") -- Play the eating sound, 'eating' is the sound name
        end,
 
        events = {
            EventHandler("animover", function(inst) -- After the 'eat' animation finishes
                inst.SoundEmitter:KillSound("eating") -- We kill the eating sound
                inst:PerformBufferedAction() -- Perform our action (ACTIONS.EAT)
                inst.sg:GoToState("idle") -- And go to state idle
            end)
        }
    }
}

Now we have states but how do we tell our mob to use them properly. Events and actions. We need an ActionHandler for eating food so we create a table with it:

Spoiler
local actionhandlers = {
    ActionHandler(ACTIONS.EAT, function() return "eat" end) -- The first parameter is the action we want to cover
                                                            -- the second is a function that should return the name of the state we want to go to
}

That covers our actions now to events. There's 2 we'll need to manage, "attacked" and "locomote". One is pushed when our mob gets attacked, the other when it wants to move:

Spoiler
local events = {
    EventHandler("attacked", function(inst) -- Listening for the "attacked" event
        if not inst.sg:HasStateTag("busy") and not inst.components.health:IsDead() then -- If our mob is not busy nor dead
            inst.sg:GoToState("hit") -- We enter the "hit" state
        end
    end),
 
    EventHandler("locomote", function(inst) -- Listening for the "locomote" event
        if not inst.sg:HasStateTag("busy") then -- If our mob is not busy
            local is_moving = inst.sg:HasStateTag("moving")
            local wants_to_move = inst.components.locomotor:WantsToMoveForward()
 
            if is_moving ~= wants_to_move then -- We check whether our mob wants to move and is not moving or the opposite
                if wants_to_move then
                    inst.sg:GoToState("premoving") -- If they want to move we go to premoving
                else
                    inst.sg:GoToState("idle") -- If they don't want to move we idle
                end
            end
        end
    end)
}

Now with that we can finalize our stategraph and return it:

Spoiler
return StateGraph("custom_mob", states, events, "idle", actionhandlers)

Finally, we set our mods stategraph with:

Spoiler
inst:SetStateGraph("SGcustom_mob")

Now our mob is starting to actually do something, however its behaviour patterns are still pretty simple, but what if we wanted something more complex, well we could use behaviours that Klei already coded for us.

5. Behaviours, like behaviour nodes?

Spoiler

Yes, behaviours are kind of like personalized nodes. They are derived from the BehavioursNode class and therefor work basically the same as normal nodes. Their main purpose is to provide interesting behaviours for mobs without having to write 100 lines of code inside our brain file. Behaviours are inside scripts/behaviours/. We used 2 behaviour nodes in What is a brain?, namely Panic and Wander.

6. A component.

Spoiler

Now it's time for components, this is where you will most likely write your mobs unique abilities and attributes. Components are big batches of code used to manage a single ability or property of a prefab. So for example some of the most used components are:

Health - manages entities health values, health penalty (for players)
Inspectable - allows the entity to be examinable by players
Combat - manages combat related properties, targeting, taking damage, dealing area damage
LocoMotor - manages entities movement, boat jumping etc.

There isn't much to talk about with components, unfortunately I won't be showing how to make you're own component in this tutorial because they're way bigger then any other subjects I've talked about in this guide already. However I will show you a few components that you should have in you mobs prefab file:

Spoiler
inst:AddComponent("locomotor") -- Adding the LocoMotor component
inst.components.locomotor.walkspeed = 4 -- Setting up walking speed
inst.components.locomotor.runspeed = 6 -- Setting up running speed
 
inst:AddComponent("combat") -- Adding the Combat component
inst.components.combat.hiteffectsymbol = "body" -- Set up hiteffectsymbol, this is the symbol any special combat fx will follow
 
inst:AddComponent("health") -- Adding the Health component
inst.components.health:SetMaxHealth(200) -- Setting up max health
 
inst:AddComponent("eater") -- Adding the Eater component
inst.components.eater:SetDiet({ FOODTYPE.MEAT }, { FOODTYPE.MEAT }) -- Setting the diet (caneat, preferseating)
inst.components.eater:SetCanEatHorrible() -- Set can eat horrible (pig skin for example)
inst.components.eater:SetStrongStomach(true) -- Set can eat monster meat without penalty
inst.components.eater:SetCanEatRawMeat(true) -- Set can eat raw meats without penalty

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

This is all for the coding part. Now all that's left is to set up some description strings and names, unless you want your mob to be named MISSING_NAME and be labeled a  t h i n g.

Inside modmain.lua you need to declare strings:

GLOBAL.STRINGS.NAMES.CUSTOM_MOB = "Custom Mob"
GLOBAL.STRINGS.CHARACTERS.GENERIC.DESCRIBE.CUSTOM_MOB = "Wilson description."
GLOBAL.STRINGS.CHARACTERS.WILLOW.DESCRIBE.CUSTOM_MOB = "Willow description."
GLOBAL.STRINGS.CHARACTERS.WOLFGANG.DESCRIBE.CUSTOM_MOB = "Wolfgang description."
GLOBAL.STRINGS.CHARACTERS.WENDY.DESCRIBE.CUSTOM_MOB = "Wendy description."
GLOBAL.STRINGS.CHARACTERS.WX78.DESCRIBE.CUSTOM_MOB = "WX78 description."
GLOBAL.STRINGS.CHARACTERS.WICKERBOTTOM.DESCRIBE.CUSTOM_MOB = "Wickerbottom description."
GLOBAL.STRINGS.CHARACTERS.WOODIE.DESCRIBE.CUSTOM_MOB = "Woodie description."
GLOBAL.STRINGS.CHARACTERS.WAXWELL.DESCRIBE.CUSTOM_MOB = "Maxwell description."
GLOBAL.STRINGS.CHARACTERS.WATHGRITHR.DESCRIBE.CUSTOM_MOB = "Wigfrid description."
GLOBAL.STRINGS.CHARACTERS.WEBBER.DESCRIBE.CUSTOM_MOB = "Webber description."
GLOBAL.STRINGS.CHARACTERS.WINONA.DESCRIBE.CUSTOM_MOB = "Winona description."
GLOBAL.STRINGS.CHARACTERS.WARLY.DESCRIBE.CUSTOM_MOB = "Warly description."
GLOBAL.STRINGS.CHARACTERS.WORTOX.DESCRIBE.CUSTOM_MOB = "Wortox description."
GLOBAL.STRINGS.CHARACTERS.WORMWOOD.DESCRIBE.CUSTOM_MOB = "Wormwood description."
GLOBAL.STRINGS.CHARACTERS.WURT.DESCRIBE.CUSTOM_MOB = "Wurt description."
GLOBAL.STRINGS.CHARACTERS.WALTER.DESCRIBE.CUSTOM_MOB = "Walter description."
GLOBAL.STRINGS.CHARACTERS.WANDA.DESCRIBE.CUSTOM_MOB = "Wanda description."

And now you're actually done! You can play around with the brain, components, maybe behaviours, and create a more complex creature. Looking through the code of already existing creatures is a very good practice. It will help you discover new ways you could code your mob.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

 

Thanks for reading and I hope this post helped you with modding your mob or better yet inspired you to create a custom mob. If there are any mistakes I've made or you simply want to ask me a question feel free to do so.

For now, here, have a cookie:

t_cookie.png.fd5724cac6084e9ba91ec1ffd34237fc.png

Edited by -LukaS-
General improvement
  • Like 15
  • Thanks 3
  • Health 1
Link to comment
Share on other sites

Hello! I'm a super newbie that decided to try modding and I used the creature mod tutorials on steam, made by Klei
(this one for example)
I ran into a problem, because my creature doesn't rotate. I have the animation playing, but it won't rotate depending on the way it walks.
The tutorial says "just add the "canrotate" tag and it will rotate the animation", but it does not work.
And it looks that the creature in the original tutorial behaves the same way, so I assume it is out of date.
I even looked in the sg files of ingame mobs and I still can't figure out where is the issue and why my animation doesn't rotate.
And the question that appears right after: how to hook up another two animations that are "front" and "back" view?

I know this might be a very stupid thing to ask, but I literally can't find any information about this anywhere.
 

Link to comment
Share on other sites

I can think of 2 problems with rotations:

1. You need to add inst.Transform:SetFourFaced() to your mobs fn (if it's supposed to have 4 facing directions). There's also SetTwoFaced()SetSixFaced() and SetEightFaced()

 

2. In the animation file you need to create 3 (or more) different facing animations. They need specific naming to work in-game. For 4 faces you need to name them:

name_up, name_side, name_down

When playing the animations you only need to use the name.

 

The Creature Tutorial mod is very, very outdated. I wouldn't try copying too many things from there. It's better to use it as a little reference when experimenting with the code. I recommend to always look for answers in the code of current version of DST.

  • Like 1
Link to comment
Share on other sites

For those who are using this and have an issue with stun locking (i had this issue even though my attack had the "busy" tag), replace
 

On 3/16/2021 at 8:58 AM, -LukaS- said:
    EventHandler("attacked", function(inst) -- Listening for the "attacked" event
        if not inst.sg:HasStateTag("busy") and not inst.components.health:IsDead() then -- If our mob is not busy nor dead
            inst.sg:GoToState("hit") -- We enter the "hit" state
        end
    end),


with

 

    EventHandler("attacked", function(inst)                                    
        if not inst.components.health:IsDead() and not inst.sg:HasStateTag("attack") and not inst.sg:HasStateTag("busy"then                       
            inst.sg:GoToState("hit")                                        
        end
    end),
Edited by Sneaky Axolxtl
  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
  • Create New...