Jump to content

Recommended Posts

Hey all,

I am trying to make a custom creature spawn from grass. I will use FIndEntities or iterate through ents (although I don't quite know how yet). My question is how do I set the function to only run once per game day for example?

Also for a spawn function, would it be better placed in modmain or a component?

It will basically work like this:

Find grass tufts
Look for entities near tufts
If none, spawn one entity
end

EDIT: Obviously I don't know what I am doing. ie:

local function populate()    if GLOBAL.GetSeasonManager() and not GLOBAL.GetSeasonManager():IsWinter() then        --local pos = Vector3(0,0,0)        local ents = GLOBAL.TheSim:FindEntities(0,0,0, 10000,"cutgrass")        for k,v in pairs(ents) do            local grasspos = v.Transform:GetWorldPosition()            local found = FindEntities(grasspos.x, grasspos.y, grasspos.z, 200, {"grasshopper"})                            if not found then                     local hopper = Global.SpawnPrefab("grasshopper")                    if hopper then                        hopper.Transform:SetPosition(grasspos.x, grasspos.y, grasspos.z)                    end                end                    end        else        local ents = GLOBAL.TheSim:FindEntities(0, 0, 0, 10000 "grasshopper")        for k,v in pairs(ents) do            v:Remove()        end    endendAddSimPostInit(populate)

which is crashing repeatedly attempting to index grasspos, which is a number.

Thanks for any help in advance.

Prof

Edited by ProfFarnsworth

Instead of going through the Ents table, you can assign this function straight to the tuft of grass.

 

function listenforday(inst)
    inst:ListenForEvent( "daycomplete", function(inst)
           -- now all you need is to search for x nearby inst and then run whatever functions you want
    end, GetWorld())
end

AddPrefabPostInit("grass", listenforday)

Edited by Heavenfall

Instead of going through the Ents table, you can assign this function straight to the tuft of grass.

 

function listenforday(inst)

    inst:ListenForEvent( "daycomplete", function(inst)

           -- now all you need is to search for x nearby inst and then run whatever functions you want

    end, GetWorld())

end

AddPrefabPostInit("grass", listenforday)

 

Thanks @Heavenfall, just a couple of questions. Would "inst" end up being the grass by default due to the PostInit function? Also, what function does GetWorld() provide, do I not need to identify what I am looking for, or that ties in to the PostInit as well?

 

I appreciate the help.

yes, the inst in this case is each individual grass piece that is running the function inside the listenforevent. The GetWorld is a paramenter for the listenforevent function, saying the inst should listen for the "daycomplete" in the world instead of in itself (default for listenforevent).

 

In other words, it is exactly the same as this

 

local function mynewfunction(inst)

    -- now all you need is to search for x nearby inst and then run whatever functions you want

 end

 

function listenforday(inst)
   inst:ListenForEvent( "daycomplete", mynewfunction(inst), GetWorld())
end

AddPrefabPostInit("grass", listenforday)

Edited by Heavenfall

yes, the inst in this case is each individual grass piece that is running the function inside the listenforevent. The GetWorld is a paramenter for the listenforevent function, saying the inst should listen for the "daycomplete" in the world instead of in itself (default for listenforevent).

 

In other words, it is exactly the same as this

 

local function mynewfunction(inst)

    -- now all you need is to search for x nearby inst and then run whatever functions you want

 end

 

function listenforday(inst)

   inst:ListenForEvent( "daycomplete", mynewfunction(inst), GetWorld())

end

AddPrefabPostInit("grass", listenforday)

Alright, I gave that a try and it got past my initial stumbling point but returns this error:

 

attempt to call global 'GetWorld' (a nil value).

 

My modified code:

function dayover(inst)    inst:ListenForEvent("daycomplete", function(inst)        if GLOBAL.GetSeasonManager() and not GLOBAL.GetSeasonManager():IsWinter() then            local pos = inst.Transform:GetWorldPosition()            local ents = GLOBAL.TheSim:FindEntities(pos.x, pos.y, pos.z, 200,"grasshopper")            for k,v in pairs(ents) do                if ents[0] == nil then                                                        local hopper = Global.SpawnPrefab("grasshopper")                    if hopper then                        hopper.Transform:SetPosition(pos.x, pos.y, pos.z)                    end                end                        end            else            local ents = GLOBAL.TheSim:FindEntities(0, 0, 0, "grasshopper")            for k,v in pairs(ents) do                v:Remove()            end        end    end, GetWorld())end

Any idea why this would be nil?

 

Thanks again.

Only certain things get put into the mod environment (the environment that the modmain.lua file gets run in). To access anything from the global environment that's not included in the mod environment, you'll have to pull from the GLOBAL table.

So, it should be GLOBAL.GetWorld()

Here's the list of things that get put into the mod environment (see mods.lua's function CreateEnvironment):

		TUNING=TUNING,		modname = modname,		pairs = pairs,		ipairs = ipairs,		print = print,		math = math,		table = table,		type = type,		string = string,		tostring = tostring,		Class = Class,		GLOBAL = _G,		MODROOT = "../mods/"..modname.."/",		Prefab = Prefab,		Asset = Asset,		Ingredient = Ingredient,
Edited by squeek

Also, there is a lot of grass in the game. If you find your game freezing for a second or two when a new day beckons, consider not running this calculation for EVERY piece of grass. For example you could have a 1 in 10 chance of running the check, or even lower.

Edited by Heavenfall

You should probably take a look at components/lureplantspawner.lua and components/penguinspawner.lua. If you only want to spawn one grasshopper per day, it makes more sense to only listen for the event in the world prefab and have a component take care of the spawning than to listen for the event on each and every grass and only spawn one grasshopper in one event (also, your code won't work; pairs will never enter a loop on an empty table, you'd need to do that nil check outside the loop [and you can check the size of an array-like table by doing # tablename, so you could do if # ents == 0 then]).

To do this, you'd want to do something like:

-- "forest" is the overworld prefabAddPrefabPostInit("forest", function(inst)  inst:AddComponent("grasshopperspawner")end)
components/grasshopperspawner.lua:

GrasshopperSpawner = Class(function(self, inst)  self.inst = inst  self.inst:ListenForEvent("daycomplete", self.SpawnGrasshopper)end)function GrasshopperSpawner:SpawnGrasshopper()  if not GetSeasonManager():IsWinter() then    local loc = self:FindSpawnLocation()    if loc then      local grasshopper = SpawnPrefab("grasshopper")      grasshopper.Transform:SetPosition(loc.x, loc.y, loc.z)    end  endendreturn GrasshopperSpawner
Edited by squeek

I probably should have explained myself better since you two are way more proficient with this code than I am :p.

 

Ideally, I would only have it run on the first day of spring and the first day of winter. I check for nearby grasshoppers because if there is one, I do not want it to spawn another (they will reproduce), and I want to kill them off in winter. Only when spring hits do I want it to spawn another grasshopper ONLY if there are not already some or even one in the vicinity.

 

@Heavenfall - 1 in 10 would be fine as well as this is only going in so people do not need to start a new world to have them spawn. If I kill them off in winter, they will need to respawn in spring.

 

Thanks to both of you. I will have a look at the reference material you provided.

GrasshopperSpawner = Class(function(self, inst)

self.inst = inst

self.has_spawned = false

self.only_spawn_offscreen = true

self.inst:ListenForEvent("seasonChange", OnSeasonChange)

end)

-- calling member functions of components directly from listeners doesn't work too well, so use a proxy

local function OnSeasonChange(inst, data)

inst.components.grasshopperspawner:OnSeasonChange(inst, data)

end

function GrasshopperSpawner:SpawnGrasshopper()

if not self.has_spawned and not GetSeasonManager():IsWinter() then

local loc = self:FindSpawnLocation()

if loc then

local grasshopper = SpawnPrefab("grasshopper")

grasshopper.Transform:SetPosition(loc.x, loc.y, loc.z)

self.has_spawned = true

end

end

end

function GrasshopperSpawner:FindSpawnLocation()

local allvalidgrass = {}

for guid,ent in pairs(Ents) do

if ent.entity:IsValid() and ent.prefab == "grass" and (not self.only_spawn_offscreen or ent:IsAsleep()) then

table.insert(allvalidgrass, ent)

end

end

-- choose a random grass

local spawngrass = GetRandomItem(allvalidgrass)

return Vector3(spawngrass.Transform:GetWorldPosition())

end

function GrasshopperSpawner:KillAllGrasshoppers()

for guid,ent in pairs(Ents) do

if ent.entity:IsValid() and ent.prefab == "grasshopper" then

-- remove offscreen/unkillable grasshoppers immediately

if ent:IsAsleep() or not ent.components.health then

ent:Remove()

-- kill onscreen grasshoppers

else

ent.components.health:Kill()

end

end

end

self.has_spawned = false

end

function GrasshopperSpawner:OnSeasonChange(inst, data)

local season = data.season

if season == SEASONS.WINTER then

self:KillAllGrasshoppers()

-- support both vanilla's 2 seasons and RoG's 4 seasons

elseif (SEASONS.SPRING ~= nil and season == SEASONS.SPRING) or (SEASONS.SPRING == nil and season == SEASONS.SUMMER) then

self:SpawnGrasshopper()

end

end

function GrasshopperSpawner:OnSave()

if self.has_spawned then

return {has_spawned = self.has_spawned}

end

end

function GrasshopperSpawner:OnLoad(data)

if data then

self.has_spawned = data.has_spawned

end

self:SpawnGrasshopper()

end

return GrasshopperSpawner

Untested code, but it should give you an idea. Also, for the KillAllGrasshoppers function, you could keep track of all grasshoppers that spawn by adding them to a list in the component and then just iterate through that instead of the global Ents table. However, it might get a tiny bit tricky because you'll have to make sure to keep that list up to date and persist it across saves. Edited by squeek

LOL, it will take me longer to go through the code than it took you to write it. xD

 

I assume "Ents" is a global variable which is why you do not use "FindEntities". If I create a component like this, could I just add it to the grasshopper prefab, or would it be better placed elsewhere, like "forest" in your post above?

LOL, it will take me longer to go through the code than it took you to write it. xD

 

I assume "Ents" is a global variable which is why you do not use "FindEntities". If I create a component like this, could I just add it to the grasshopper prefab, or would it be better placed elsewhere, like "forest" in your post above?

The component will only work when it's added to the world prefab (for the overworld, that would be "forest") because the "seasonChange" event will only get pushed from the world prefab, and the component assumes that self.inst is the world. I'm not sure I explained that right, but, basically, the component should only be added to the world prefab and it should be treated as a singleton (only one should exist at a time).

For grasshoppers reproducing, you should use the "periodicspawner" component instead. Here's how it's used in beefalo reproduction (see prefabs/beefaloherd.lua):

    inst:AddComponent("periodicspawner")    inst.components.periodicspawner:SetRandomTimes(TUNING.BEEFALO_MATING_SEASON_BABYDELAY, TUNING.BEEFALO_MATING_SEASON_BABYDELAY_VARIANCE)    inst.components.periodicspawner:SetPrefab("babybeefalo")    inst.components.periodicspawner:SetOnSpawnFn(OnSpawned)    inst.components.periodicspawner:SetSpawnTestFn(CanSpawn)    inst.components.periodicspawner:SetDensityInRange(20, 6)    inst.components.periodicspawner:SetOnlySpawnOffscreen(true)
The Ents table is used instead of TheSim:FindEntities for the reasons Heavenfall stated here (no unnecessary distance checks). Edited by squeek

Suggestion:

 

To avoid any chance of causing lag with a ton of grass objects in an arbitrarily large world I would probably do things in the following order.

 

1. only have the World object listen for season changes

 

2. in the callback that respond to a season change:

2.a cycle through all entities to find grass objects

2.b schedule a delayed task with a random time span covering the daylight hours of the first day to run on each grass object found

 

3. in that task handler:

3.a do your 'find nearby grasshoppers check'

3.b spawn a grasshopper if not found

 

The reason for this slightly altered setup would be that running tons of  'find nearby' checks can be computationally expensive. You wouldnt want to risk doing too many of them in the exact same update tick. 

 

You can actually optimize it further by not cycling through the global entity list and instead maintain your own table of all grass instances. This would make the list navigation faster at the cost of slightly higher RAM requirement. I actually recommend NOT doing this though. The memory requirement, while not huge, would still be constant. And it would only be speeding up an action that occurs on 2 days out of a game year.

Edited by seronis

seronis you might have missed a few posts. He actually wants to spawn a single grasshopper from a single (random) grass once per year. The code I posted does function very similarly to your suggested method though (only listen for season changes from the world prefab).

Edited by squeek

Even rereading it now I was understanding it as still wanting to check all grass but just not spawn a grasshopper if any are already 'in the vacinity' (i would assume that was limited to 1 screen distance, not the entire world).  Which would make it so that a huge field of grass didnt spawn a field of grasshoppers without limiting it more than that.

@seronis - Thank you for the suggestions, it is a bit over my head right now apparently. you have the right idea though. (I just found "Oh poop", great mod, very original)

 

@squeek - I tried your code above and am getting an unusual error (unusual to me anyway)

 

components/grasshopperspawner.lua:13: function arguments expected near 'then'

 

in this function:

function GrasshopperSpawner:SpawnGrasshopper()    if not self.has_spawned and not GetSeasonManager():IsWinter then --error here apparently        local loc = self:FindSpawnLocation()        if loc then            local grasshopper = SpawnPrefab("grasshopper")            grasshopper.Transform:SetPosition(loc.x, loc.y, loc.z)            self.has_spawned = true        end    endend

which is odd as I thought I had arguments on both sides of "then". I do appreciate all the help as this is my first time writing in Lua.

 

Any ideas?

Can't thank you enough squeek, but up rep is all I can do.

 

Will let you know how it goes.

 

EDIT: Ok, so I had to change the proxy OnSeasonChange from a local to a global function, otherwise it was crashing saying it was an undeclared variable, and it works beautifully. However, it only spawns one grasshopper in the whole world. I am going to try to add a loop around the body of SpawnGrasshopper to spawn more around the world. If you guys know a better way, please let me know.

Edited by ProfFarnsworth

No problem. By the way, if you don't understand something or are unsure of how something works, make liberal use of the print function.

For example, if you modify GrasshopperSpawner:FindSpawnLocation like so:

function GrasshopperSpawner:FindSpawnLocation()    print("Finding spawn location for Grasshopper...")    local allvalidgrass = {}    for guid,ent in pairs(Ents) do        if ent.entity:IsValid() and ent.prefab == "grass" and (not self.only_spawn_offscreen or ent:IsAsleep()) then            table.insert(allvalidgrass, ent)        end    end    print("Found "..(# allvalidgrass).." valid grass entities to spawn from")     -- choose a random grass    local spawngrass = GetRandomItem(allvalidgrass)    print("Chosen spawn: "..tostring(spawngrass))    return Vector3(spawngrass.Transform:GetWorldPosition())end
it'll give you a better idea of how and when it's choosing spawn points. Just make sure to use tostring() often when appending variables to strings, because if a variable is ever nil/a boolean/etc it'll give you an error if you don't use tostring() on it.

EDIT: Should probably mention just in case that you can bring up the console log (where that stuff will be printed to) by using Ctrl + L and you can bring up the console itself using the ~ key

EDIT#2: Oh, and take those prints out when you release your mod. Don't want to spam people's consoles with random information that's only useful for debugging. :-)

Edited by squeek

Well, you guys rock. I would have given up and just told people to start a new world, and had grasshoppers jumping around all winter, as evidenced by the code in my OP. I had no idea this simple feature would be so involved, so huge thanks to each of you.

 

@squeek - I have been using the console extremely liberally to start seasons, change time, etc. Your code showed me a ton of functions I didn't even know existed. I just had to move "self.has_spawned = true" outside the loop, and come spring, grasshoppers everywhere! Too funny to see them keel over on the first day of winter too.

 

@seronis, @Heavenfall, @squeek: if you guys want co-author, just send me your steam ID. You're getting credit on the mod page anyway unless expressly forbidden.

 

So happy to have this mod wrapped up (except for some animations getting cutoff, which I may or may not fix). This mod was made for me to learn about modding don't starve, and now I know so much more. So appreciative!!

Edited by ProfFarnsworth

@seronis, @Heavenfall, @squeek: if you guys want co-author, just send me your steam ID. You're getting credit on the mod page anyway unless expressly forbidden.

Not bothered about credit. Glad I could help. Edited by squeek

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
×
  • Create New...