Jump to content

Best way to count kills?


havenphillip
 Share

Recommended Posts

It should not be like self:enemyDied() as that's calling it at that time. When you do self.enemyDied you're just passing it around like a variable which is what you want.

Copy/paste your entire Main.lua file here and I'll look through it. Then copy/paste the relevant parts of player and monster as well.

 

Link to comment
Share on other sites

Ok. Here's my main:

import("Scripts/Menu.lua")

events = {}
subId = 0

eventCoroutines = {}

function SubscribeEvent(eventName, script, func)
    -- check to see if this event name exists already or not and if not create a new table for the event
    -- we do this because we can have many subscribers to one event
    if events[eventName] == nil then
        events[eventName] = {}
    end

    -- increase our eventId by 1
    subId = subId + 1

    -- add this script function to our list of subscribers for this event
    -- one event can have many subscribers that need to know about it for various reasons
    events[eventName][subId] = {
        scriptObject = script,
        scriptFunction = func
    }

    -- return this subId id so the subscriber can unsubscribe if they need to
    return subId
end

function Unsubscribe(eventName, subId)
    if events[EventName] == null then return end

    -- remove this subscription for this event
    events[EventName][subId] = nil
end

function RaiseEvent(eventName, data)
    -- if someone tried to raise an event that doesn't have an entry in our events table do nothing
    if events[eventName] == null then return end

    -- loop through all the subscriptions for this event (there may be many game entities who want to know about this event)
    for i = 1, #events[eventName] do
        -- get the script and function
        local scriptFunc = events[eventName].scriptFunction
        local script = events[eventName].scriptObject
        
        -- insert the functions into the eventCoroutines table. this will be iterated over in the main game loop below and resumed into
        table.insert(eventCoroutines, {
            co = coroutine.create(scriptFunc),
            args = data,
            script = script
        })
    end
end

function WaitForSeconds(interval)
    local tm = Time:GetCurrent()

    while Time:GetCurrent() <= tm + (interval * 1000) do
        coroutine.yield()
    end
end

--Initialize Steamworks (optional)
--Steamworks:Initialize()

--Initialize analytics (optional).  Create an account at www.gameamalytics.com to get your game keys
--[[if DEBUG==false then
    Analytics:SetKeys("GAME_KEY_xxxxxxxxx", "SECRET_KEY_xxxxxxxxx")
    Analytics:Enable()
end]]

--Set the application title
title="TEMP"

--Create a window
local windowstyle = 0
local winwidth
local winheight
local gfxmode = System:GetGraphicsMode(System:CountGraphicsModes()-1)
if System:GetProperty("devmode")=="1" then
    gfxmode.x = math.min(1280,gfxmode.x)
    gfxmode.y = Math:Round(gfxmode.x * 9 / 16)
    windowstyle = Window.Titlebar
else
    gfxmode.x = System:GetProperty("screenwidth",gfxmode.x)
    gfxmode.y = System:GetProperty("screenheight",gfxmode.y)
    windowstyle = Window.Fullscreen
end
window = Window:Create(title,0,0,gfxmode.x,gfxmode.y,windowstyle)
if window == nil then
    gfxmode = System:GetGraphicsMode(System:CountGraphicsModes()-1)
    window = Window:Create(title,0,0,gfxmode.x,gfxmode.y,windowstyle)
end

--Create the graphics context
context=Context:Create(window,0)
if context==nil then return end

--Create a world
world=World:Create()

local gamemenu = BuildMenu(context)

--Load a map
local mapfile = System:GetProperty("map","Maps/start.map")
if mapfile~="" then
    if Map:Load(mapfile)==false then return end
    prevmapname = FileSystem:StripAll(changemapname)
    
    --Send analytics event
    Analytics:SendProgressEvent("Start",prevmapname)
    
    gamemenu.newbutton:Hide()
    gamemenu.resumebutton:Show()
    window:HideMouse()
else
    gamemenu:Show()
end

while window:Closed()==false do
    
    --Show game menu when escape key is hit
    if gamemenu:Hidden() then
        if window:KeyHit(Key.Escape) then
            Time:Pause()
            gamemenu:Show()
        end
    end

    --Update events
    while EventQueue:Peek() do
        local event = EventQueue:Wait()
        event = gamemenu:ProcessEvent(event)
    end
    
    --Handle map change
    if changemapname~=nil then
        
        --Pause the clock
        Time:Pause()
        
        --Pause garbage collection
        System:GCSuspend()        
        
        --Clear all entities
        world:Clear()
        
        --Send analytics event
        Analytics:SendProgressEvent("Complete",prevmapname)
        
        --Load the next map
        if Map:Load("Maps/"..changemapname..".map")==false then return end
        prevmapname = changemapname
        
        --Send analytics event
        Analytics:SendProgressEvent("Start",prevmapname)
        
        --Resume garbage collection
        System:GCResume()
        
        --Resume the clock
        Time:Resume()
        
        changemapname = nil
    end    
    
    if gamemenu:Hidden() then
        
        --Update the app timing
        Time:Update()

        -- loop over backwards so we can safely remove event function coroutines that are finished
        for i = #eventCoroutines, 1, -1 do
            if coroutine.status(eventCoroutines.co) == "dead" then
                table.remove(eventCoroutines, i)
            else
                -- go back into the event function passing the script as the first param so it ends up being 'self' inside the function and args as the second parameter
                coroutine.resume(eventCoroutines.co, eventCoroutines.script, eventCoroutines.args)
            end
        end

        --Update the world
        world:Update()
        
    end

    --Render the world
    world:Render()
        
    --Render statistics
    context:SetBlendMode(Blend.Alpha)
    if DEBUG then
        context:SetColor(1,0,0,1)
        context:DrawText("Debug Mode",2,2)
        context:SetColor(1,1,1,1)
        context:DrawStats(2,22)
        context:SetBlendMode(Blend.Solid)
    else
        --Toggle statistics on and off
        if (window:KeyHit(Key.F11)) then showstats = not showstats end
        if showstats then
            context:SetColor(1,1,1,1)
            context:DrawText("FPS: "..Math:Round(Time:UPS()),2,2)
        end
    end
    
    --Refresh the screen
    if VSyncMode==nil then VSyncMode=true end
    context:Sync(VSyncMode)
    
end

Link to comment
Share on other sites

Here's the relevant part of the player:

function Script:enemyDied(data)

    WaitForSeconds(2.5);

    System:Print("Enemy died")

    WaitForSeconds(1.0);

    System:Print("Wow this is cool!")
end

--This function will be called when an entity is loaded in a map.  Use this for intitial setup stuff.
function Script:Start()
    self.onDeadId = SubscribeEvent("onDead", self, self.enemyDied)

...



And the relevant part of the crawler:

function Script:Hurt(damage,distributorOfPain)
    if self.health>0 then
        if self.target==nil then
            self.target=distributorOfPain
            self:SetMode("attack")
        end

        self.health = self.health - (damage + math.random(-4,4))

        if self.health<=0 then
            self.entity:SetMass(0)
            self.entity:SetCollisionType(0)
            self.entity:SetPhysicsMode(Entity.RigidBodyPhysics)
            self:SetMode("dying")
            self:RaiseEvent("onDead", {})
        end
    end
end

 

 

Link to comment
Share on other sites

In the crawler script you're doing self:RaiseEvent(), remove the self: and just call RaiseEvent("onDead", {}).

RaiseEvent() is a global function and when you do self anything that refers to the current script which is not what you want in this case. You want to call the global function RaiseEvent

Link to comment
Share on other sites

I forgot I had pasted in those other two functions. I combined the two "enemyDied(data)"  and it works now:

function Script:enemyDied(data)
    self.kills = self.kills + 1
    WaitForSeconds(2.5);

    System:Print("Enemy died")

    WaitForSeconds(1.0);

    System:Print("Wow this is cool!")
end

Link to comment
Share on other sites

function Unsubscribe(eventName, subId)
    if events[EventName] == null then return end

    -- remove this subscription for this event
    events[EventName][subId] = nil
end

 

Remember to fix the typo in this function too. Inside the function it's using EventName instead of eventName.

 

8 hours ago, havenphillip said:

So that's like how in the video you put a function inside a function.

Right. This idea is know as a callback. You pass a function to some other system and that system will call that function at some later time. So when you subscribe to an event you're passing a function to the event system so that when the event is raised the event system can "call back" the function. Because the function you are wanting to be called back is part of a table (the Script table) you need to do 2 things. First you need to send the script itself which we do by passing 'self' and then the function which we do by passing self.FunctionName and stores it in a variable. The event system eventually calls the function variable passing in the script itself as the first parameter. When your table functions are defined with the colon like Script:MyFunction() and you call it like functionVariable(table) it automatically assigns that first parameter as 'self' behind the scenes which is why inside Script:MyFunction() you can use the self variable to refer to the script table itself.

So what you have is all good the rest here is just some details about Lua and how it works:

If it was a regular function (non table function) then we would just pass the function itself and be done with it. Functions are really just variables so you can define functions like:

-- define a function

myFunc = function() System:Print("Test") end

function FunctionThatTakesACallback(func)

-- call the passed in function

 func()

end

-- pass our function to this other function and that'll call it

FunctionThatTakesACallback(myFunc)

 

If you don't care to store the function in a variable you can use a shortcut and just define the function right in the parameter of the other function like:

-- this results in the same thing it's just a shortcut of not storing our Test function to a variable before passing it to the function

FunctionThatTakesACallback(function()

   System:Print("Test")

end)

Javascript does a lot of this idea of anonymous functions being passed to other functions. It's referred to anonymous because the function doesn't have a name.

Link to comment
Share on other sites

Dude this is like chaos to me. Probably because it's coming all at once. I'm having a hard time seeing the process from beginning to end in a coherent line. I want to grasp it because I can see that it gives you a lot of control over events and sequences of events and I assume it's a more "industry-standard" way to do things. I looked up "callback" and it kind of makes sense that its passing a function as a variable from one "system" to another, which then executes (or "calls back") that function within the limits that you put on it in the parent function. So when you "subscribe" to an event the coroutine basically just grabs it or gets involved in the process, tells it to do some things, and then "unsubscribes" from it. It's vaguely familiar how you're passing information around in parentheses  and defining them somewhere else because I've seen some of that in the shaders.

Maybe if you have the time (and/or patience) you could walk me through how I could use this to use context:DrawText() to put "enemy killed" on the screen instead of in the System:Print(). Would that be easy? That was something I was going to try to figure out after I got the kill-counter working and I was trying to think of how I could set it when the enemy is killed and then wait a few seconds and then delete it, and it seems like this may be the way to do that. Eventually I want to set it up to say something like "entity.name.." was killed by player" and then set that in a table that iterates each name so I might have several names on the screen at one time (basically like a list of recent kills). I can maybe figure out that last part on my own later. But like what's step one?

I made another script called "Coroutine"  (should I call it that?) for that part that I put in the Main.lua, and then in the main put " import("Scripts/Coroutine.lua")  but I left this part because I wasn't sure how to import it into that specific place.

        -- loop over backwards so we can safely remove event function coroutines that are finished
        for i = #eventCoroutines, 1, -1 do
            if coroutine.status(eventCoroutines.co) == "dead" then
                table.remove(eventCoroutines, i)
            else
                -- go back into the event function passing the script as the first param so it ends up being 'self' inside the function and args as the second parameter
                coroutine.resume(eventCoroutines.co, eventCoroutines.script, eventCoroutines.args)
            end
        end

This is the idea:

 

waskilled.png

Link to comment
Share on other sites

21 minutes ago, havenphillip said:

Maybe if you have the time (and/or patience) you could walk me through how I could use this to use context:DrawText() to put "enemy killed" on the screen instead of in the System:Print(). Would that be easy? That was something I was going to try to figure out after I got the kill-counter working and I was trying to think of how I could set it when the enemy is killed and then wait a few seconds and then delete it, and it seems like this may be the way to do that.

It's very easy to do with this actually. This is actually where coroutines shine! Doing things for set periods of time or over multiple frames. So assuming your UI stuff is inside your player script, you'd probably want to create a variable that will hold the text message to display on the screen.

Inside Script:Start() self.killMessage = ""

Since it's blank you can actually do context:DrawText() all the time in your render2D function (is that the script name? I forget). It just won't draw anything on the screen if it's blank. Then the idea is that in your enemyDead() script function you set the self.killMessage to whatever you want, then call that WaitForSeconds() and after set it back to empty string. It'll then show up on the screen when you kill someone, sit there for however many seconds you want then go away! Super easy.

Now imagine you want the text to fade away instead of snap away (very polished stuff)! No problem, make another variable in Start called like self.killMessageAlpha = 1. In the render function before you draw self.killMessage set the color where the alpha value is this variable context:SetColor(Vec4(1, 1, 1, self.killMessageAlpha). Then in your enemyDead function after you've waited  your seconds of displaying instead of just setting self.killMessage to empty string the idea is to make a loop where you decrease self.killMessageAlpha a little each iteration while yielding inside the loop. You could do something like:

 

-- player around with how much you subtract and how long you wait for to get the smoothness of fading out you like

while self.killMessageAlpha >= 0 do

   self.killMessageAlpha = self.killMessageAlpha - .01

   WaitForSeconds(0.25)

end

-- reset the variables after we've faded out this message

self.killMessage = ""

self.killMessageAlpha = 1

 

So as you can see the idea is to create variables in your script and you can manipulate them over multiple frames easily enough inside one of these event function callbacks via coroutines.

 

On the topic of moving it to another file, I wouldn't name it coroutines. Coroutines is just a programming language feature. Really it's an EventSystem so that's probably a better name. As far as that part in the main loop, you can add a function to your EventSytem file named something like UpdateEventSystem() and put that code in there, then call this function in Main where that code was.

 

As far as understanding the code flow, yeah coroutines and callbacks can be confusing if you've never worked with them. Coroutines especially can be hard. So on subscription we are just storing the function callbacks with the event string name. That's it really at that point. One event string name can have many different function callbacks linked to it. When an event is raised is where things get interesting. So before we added coroutines to this all, those callback functions were just be called right there. It would just loop through all callbacks subscribed to that event string name and call them right then and there. I think that's somewhat easy to understand. You stored the functions in subscribe and call them for a given event name in raise. Each function would start at the top and go to the bottom and finish just like a normal function.

We changed that when we added coroutines. Now inside raise event instead of looping through and calling the function, we loop through and get the function for the raised event string name and create a coroutine variable from it and store it in a separate table. That's all raise event does now. Then we have that loop in our main game loop that will loop over these coroutines in this other coroutine table and with coroutines you "resume" into them instead of just call them once and done. In one iteration it'll go into each function but if inside it sees a coroutine.yield() statement it'll come back out to the loop, but it remembers everything about where it left off in that function so the next game loop when we loop over that table of coroutines and resume back into them it'll pick up where they left off the last time. Once it reaches the end of any function that coroutine is marked with a status of "dead" and we'll remove it from the table of coroutines so we don't iterate over it anymore. But if that event gets raised again, it'll repeat this entire process. So in that vein you could have multiple coroutines calling that same function if you kill enemies fast enough and the last one isn't finished yet. Something to test on how it behaves.

 

I'm a very patient person and I like teaching so keep asking those question! :)

 

Link to comment
Share on other sites

Ok cool! Because I tend to have a lot of questions. I totally want to learn this now that I'm seeing what it can do.

I think I get this: " One event string name can have many different function callbacks linked to it... before we added coroutines to this all, those callback functions were just be called right there... Now inside raise event instead of looping through and calling the function, we loop through and get the function for the raised event string name and create a coroutine variable from it and store it in a separate table."

So basically the event system grabs the information by the string name and puts it into a table so it can loop everything, runs it through that extra loop within itself, adding whatever you want to it, before passing it back into its original loop? So basically this just pulls out a piece of information and stores it, sends it around an outside loop, then puts it back? Like a track that you switch so the train (string name) always makes an extra stop?

Here's the parts that I have. I'm not getting anything currently. I'm not sure what you mean by "render2D function"? I'm doing the drawtext in the player script under the PostRender function.

In the player script:

function Script:enemyDied(data)

    self.kills = self.kills + 1

    WaitForSeconds(2.5);

    self.killMessage = ""  ---do I write the context:DrawText(...) here?

    WaitForSeconds(1.0);

    while self.killMessageAlpha >= 0 do

        self.killMessageAlpha = self.killMessageAlpha - .01

        WaitForSeconds(0.25)

        self.killMessage = ""

        self.killMessageAlpha = 1
    end
end

function Script:CleanUp()
    Unsubscribe(self.onDeadId)
    Unsubscribe(self.killMessage)
end

--This function will be called when an entity is loaded in a map.  Use this for intitial setup stuff.
function Script:Start()

    self.onDeadId = SubscribeEvent("onDead", self, self.enemyDied)
    self.killMessage = ""
    self.killMessageAlpha = 1

...

...and later in the PostRender just:

    --draw kills
    context:SetBlendMode(1)
    context:SetColor(1,1,1,1)
    context:DrawText("Kills: "..self.kills,30,30,200,200) --- Do I need to say "self.killMessage = " here first? Edit: My bad, this is just the kills. But is it good                                                                                                                                                                                write the other text here?

...

I changed the name of my "Coroutine" script to "Event System" and did like you said in the Main.lua just added "BackLoop()" between the Time and World update. That seems to be working.

And my crawler script still looks the same. Do I need to add something there? or I guess this happens in the same "onDead" event.

function Script:Hurt(damage,distributorOfPain)

...

    if self.health<=0 then
            self.entity:SetMass(0)
            self.entity:SetCollisionType(0)
            self.entity:SetPhysicsMode(Entity.RigidBodyPhysics)
            self:SetMode("dying")
            RaiseEvent("onDead", {})
        end
    end
end

Link to comment
Share on other sites

PostRender() that's it! Yes, you want to draw your self.killMessage text in post render. That's the only way it'll show up on the screen. The context:DrawText() has to always be in PostRender() otherwise it won't show up.

 

 

function Script:enemyDied(data)

    self.kills = self.kills + 1

    WaitForSeconds(2.5);

    self.killMessage = "Player Killed Enemy"  -- Note: if you wanted to know what kind of enemy you could have that passed into the data parameter from where you RaiseEvent (remember we passed an empty object {} but you could do something like { enemyName = "Monster" } in the RaiseEvent() and read data.enemyName here if you wanted.

    WaitForSeconds(1.0);

    while self.killMessageAlpha > 0 do

        self.killMessageAlpha = self.killMessageAlpha - .01

        WaitForSeconds(0.25)
    end

        self.killMessage = ""    -- this has to be outside the loop so the message is cleared AFTER the alpha has reached 0

        self.killMessageAlpha = 1
end

 

 --draw kills
    context:SetBlendMode(1)
    context:SetColor(1,1,1,1)
    context:DrawText("Kills: "..self.kills,30,30,200,200)

   context:SetColor(Vec4(1, 1, 1, self.killMessageAlpha))

   context:DrawText(self.killMessage, 30, 50, 200, 200)

 

function Script:CleanUp()
    Unsubscribe(self.onDeadId)
    Unsubscribe(self.killMessage)    -- NOTE: No need to do this. self.killMessage is just a normal string variable not an event like self.onDeadId is. You only Unsubscribe() for events
end

Link to comment
Share on other sites

This is cool, man. It's totally working. In the crawler script I set it like this:

RaiseEvent("onDead", { enemyName = self.name.." was killed by player" })

and in the player script under enemyDied(data) like you said:

self.killMessage = data.enemyName

What's another event that this would be good for? I want to try and see if I can follow the pattern and piece one together myself.

I'm going to also see if I can get the names to list downward with a table iteration. That ought to keep me going for awhile.

Link to comment
Share on other sites

20 minutes ago, havenphillip said:

What's another event that this would be good for? I want to try and see if I can follow the pattern and piece one together myself.

That would all depend on your game really. I don't know much about your game so it's hard for me to say. But, let's say you have enemies scattered around your level. If you wanted an enemy to be able to alert fellow enemies around them of the player when the player attacks it you could use an event for that. Your monster script would subscribe for an "onAttacked" event and inside the monster Hurt() function you could raise "onAttacked" event. All the other monsters would get that event. Now you wouldn't want ALL monsters to come charging but maybe all monsters in a radius of the monster that was attacked? In that case when you raise "onAttacked" event you can send in the parameter data that monsters position. Then in the subscribed function that you linked to the onAttacked event you can take THAT monsters location and get the distance to the passed in monsters location and if within a certain range set that monsters target to the player (I suppose that means you'd also have to pass the player (the attacker, which you have in the Hurt() function) to the onAttacked event data parameter as well so it can set the target those monsters should attack.

 

-- I don't remember the exact function signature of Hurt but you get the idea

function Script:Hurt(amount, sourceOfPain)

   RaiseEvent("onAttacked", { hurtMonsterPosition = self.entity:GetPosition(), player = sourceOfPain })

end

-- if this is the function linked to the onAttacked event

function Script:onAttacked(data)

   -- I don't recall if this is the correct syntax for distance checks but it gets the idea across. you'll have to research distance checks.

   if data.hurtMonsterPosition:Distance(self.entity:GetPosition) < 5 then

      -- if this monster is in a given range of the monster that was attacked then assign this monster the player target as well and it'll come running towards it!

      self.target = data.player

   end

end

 

Anytime you need to communicate between different entities is where these events can come into play. This system is one tool in your tool belt and it has a usage of inter entity communication.

 

Link to comment
Share on other sites

That one doesn't seem to be doing anything. I have it set up in the crawler script like:

    function Script:Hurt(damage,DistributorofPain)

        RaiseEvent("onAttacked", { hurtMonsterPosition = self.entity:GetPosition(true), player = distributorOfPain })  -- the script doesn't reference "player" anywhere else. Is it that? 

     -- if this is the function linked to the onAttacked event
    function Script:onAttacked(data)

      -- I don't recall if this is the correct syntax for distance checks but it gets the idea across. you'll have to research distance checks.

       if data.hurtMonsterPosition:GetDistance(self.entity:GetPosition(),true) < 500 then

          -- if this monster is in a given range of the monster that was attacked then assign this monster the player target as well and it'll come running towards it!

          self.target = data.player

       end

    end

Link to comment
Share on other sites

I'm getting "attempt to index a nil value" on this line in the event system script:

        local scriptFunc = events[eventName].scriptFunction

 

I had added these to the crawler script:

       function Script:Start()

       self.onAttackedId = SubscribeEvent("onAttacked", self, self.onAttacked)
  

...

       function Script:CleanUp() -- (Rick)
           Unsubscribe(self.onAttackedId)
       end

 

Just tried to mimic what you did before.

Link to comment
Share on other sites

So that error is happening in the RaiseEvent() side of things. That first line of code you have there isn't correct though is it? 

local scriptFunc = events[eventName][i].scriptFunction

I think that's how it looks right? (Your code as missing the part).

 

It's hard to piece mail this stuff but you have the function Script:onAttacked() function in the crawler script too right? That's needed in that script. I would need to see the relevant parts of each script to determine exactly why it's happening.

Link to comment
Share on other sites

Oh yeah. That's weird. Because it's written correctly in the code.

This is all the code I added. It's all in the crawler script:

function Script:Start()

...

    self.onAttackedId = SubscribeEvent("onAttacked", self, self.onAttacked) --subscribed to event
end

function Script:CleanUp() -- (Rick)
    Unsubscribe(self.onAttackedId) --unsubscribed to event
end
 

function Script:Hurt(damage,distributorOfPain)
    if self.health>0 then
        player = self.target
        RaiseEvent("onAttacked", { hurtMonsterPosition = self.entity:GetPosition(true), player = distributorOfPain }) --- raise event

...

 -- if this is the function linked to the onAttacked event
function Script:onAttacked(data) --raise event function

   -- I don't recall if this is the correct syntax for distance checks but it gets the idea across. you'll have to research distance checks.

   if data.hurtMonsterPosition:GetDistance(self.entity:GetPosition(),false) < 5 then

      -- if this monster is in a given range of the monster that was attacked then assign this monster the player target as well and it'll come running towards it!

      self.target = data.player

   end

end

In the original kills thing you you showed me you wrote this:

function Script:Start()

    self.onDeadId = SubscribeEvent("onDead", self, self.enemyDied)

...I was trying to figure out where you got "self.onDeadId" I just tried to mimic that in the crawler script.

I also tried setting "player = self.target"

How would it know what "player" means in the Hurt()?

I'm getting there slowly.

Link to comment
Share on other sites

Sure. Here you go:

import "Scripts/Functions/GetEntityNeighbors.lua"

--Public values
Script.name = ""
Script.despawnTime = 60 --int "Despawn Time"
Script.despawnTimer = 0
Script.removeBodyTime = 8
Script.removeBodyTimer = 0
Script.maxhealth=40 --int "Max Health"
Script.health=40--int "Health"
Script.enabled=true--bool "Enabled"
Script.target=nil--entity "Target"
Script.sightradius=30--float "Sight Range"
Script.senseradius=2--float "Hearing Range"
Script.teamid=2--choice "Team" "Neutral,Good,Bad"
Script.attackdelay=300--int "Attack delay"
Script.animspeedrun=0.03--float "Run anim speed"
Script.animspeedDeath= 0.028 --float "Death anim Speed"
Script.speed= 5 --float "Move Speed"


--Private values
Script.damage=20 + math.random(-10,10)
Script.attackrange=1.7
Script.updatefrequency=500
Script.lastupdatetime=0
Script.prevtarget=nil
Script.followingtarget=false
Script.maxaccel=15
Script.lastupdatetargettime=0
Script.attackmode=1
Script.attackbegan=0
Script.attack1sound=""--path "Attack 1 sound" "Wav file (*.wav):wav|Sound"
Script.attack2sound=""--path "Attack 2 sound" "Wav file (*.wav):wav|Sound"
Script.alertsound=""--path "Alert sound" "Wav file (*.wav):wav|Sound"
Script.deathsound=""--path "Death sound" "Wav file (*.wav):wav|Sound"
Script.idlesound=""--path "Idle sound" "Wav file (*.wav):wav|Sound"

Script.headshotDmg = 7 --headshot multiplier

function Script:Enable()--in
    if self.enabled==false then
        if self.health>0 then
            self.enabled=true
            if self.target~=nil then
                self:SetMode("roam")
            else
                self:SetMode("idle")
            end
        end
    end
end

function Script:ChooseTarget()
    local entities = GetEntityNeighbors(self.entity,self.sightradius,true)
    local k,entity
    for k,entity in pairs(entities) do
        if entity.script.teamid~=nil and entity.script.teamid~=0 and entity.script.teamid~=self.teamid then
            if entity.script.health>0 then
                local d = self.entity:GetDistance(entity)
                local pickinfo=PickInfo()
                if self.entity.world:Pick(self.entity:GetPosition()+Vec3(0,1.6,0),entity:GetPosition()+Vec3(0,1.6,0),pickinfo,0,false,Collision.LineOfSight)==false then
                    if d < self.sightradius then --added so they don't charge from any distance
                    return entity.script
                    end
                end
            end
        end
    end
end

function Script:DistanceToTarget()
    local pos = self.entity:GetPosition()
    local targetpos = self.target.entity:GetPosition()
    if math.abs(targetpos.y-pos.y)<1.5 then
        return pos:xz():DistanceToPoint(targetpos:xz())
    else
        return 100000--if they are on different vertical levels, assume they can't be reached
    end
end

function Script:TargetInRange()
    local pos = self.entity:GetPosition()
    local targetpos = self.target.entity:GetPosition()
    if math.abs(targetpos.y-pos.y)<1.5 then
        if pos:xz():DistanceToPoint(targetpos:xz())<self.attackrange then
            return true
        end
    end
    return false
end

function Script:Start()

    self.entity:SetKeyValue("type","enemy")
    self.despawnTimer = 0

    --Handle default parameters
    if speed==nil then speed=5.0 end
    if blendtime==nil then blendtime=500 end
    if  mode==nil then mode=0 end

    if self.entity:GetMass()==0 then
        self.entity:SetMass(10)
    end

    self.entity:SetPickMode(Entity.BoxPick,true)
    self.entity:SetPickMode(1,false)
    self.entity:SetPhysicsMode(Entity.CharacterPhysics)
    self.entity:SetCollisionType(Collision.Prop,true)
    self.entity:SetCollisionType(Collision.Character,false)
    if self.enabled then
        if self.target~=nil then
            self:SetMode("roam")
        else
            self:SetMode("idle")
        end
    end
    self.sound={}
    if self.alertsound then self.sound.alert = Sound:Load(self.alertsound) end
    self.sound.attack={}
    if self.attack1sound then self.sound.attack[1] = Sound:Load(self.attack1sound) end
    if self.attack2sound then self.sound.attack[2] = Sound:Load(self.attack2sound) end
    if self.idlesound then self.sound.idle = Sound:Load(self.idlesound) end
    self.lastidlesoundtime=Time:GetCurrent()+math.random(1,20000)
    
    --Headshot Box
    self.headshotbox = self.entity:FindChild("Head") -- or whatever you name the head limb in the model tree
    if self.headshotbox ~= nil then
        self.headshotbox.script.parent = self.entity
        self.headshotbox.script.health = self.health
        self.headshotbox.script.damageMulti = self.headshotDmg
    end

    self.onAttackedId = SubscribeEvent("onAttacked", self, self.onAttacked)
end

function Script:CleanUp() -- (Rick)
    Unsubscribe(self.onAttackedId)
end
 

function Script:Hurt(damage,distributorOfPain)
    RaiseEvent("onAttacked", { hurtMonsterPosition = self.entity:GetPosition(), player = distributorOfPain})

    if self.health>0 then
        if self.target==nil then
            self.target=distributorOfPain
            self:SetMode("attack")
        end


        self.health = self.health - (damage + math.random(-4,4))
        --local entity = Prefab:Load("Damage numbers/Damage number.pfb")
        --entity:SetPosition(self.entity:GetPosition(true)+Vec3(0,2,0))
        --entity.script:CreateDamageNumber(damage + math.random(-1,3),0.7,size,color,Vec3(0,0.02,0))

        if self.health<=0 then
            self.headshotbox.script.health = 0
            self.entity:SetMass(0)
            self.entity:SetCollisionType(0)
            self.entity:SetPhysicsMode(Entity.RigidBodyPhysics)
            self:SetMode("dying")
            self:DropLoot()
            RaiseEvent("onDead", { enemyName = self.name.." was killed by player" }) --(Rick)
        end
    end
end

 -- if this is the function linked to the onAttacked event
function Script:onAttacked(data)
    System:Print("Yeah it's calling this function")
   -- I don't recall if this is the correct syntax for distance checks but it gets the idea across. you'll have to research distance checks.

   --if data.hurtMonsterPosition:GetDistance(self.entity:GetPosition(),true) < 500 then

      -- if this monster is in a given range of the monster that was attacked then assign this monster the player target as well and it'll come running towards it!

    self.target = data.player

  -- end

end
function Script:EndDeath()
    self:SetMode("dead")
end

function Script:DirectMoveToTarget()
    self.entity:Stop()
    local targetpos = self.target.entity:GetPosition()
    local pos = self.entity:GetPosition()
    local dir = Vec2(targetpos.z-pos.z,targetpos.x-pos.x):Normalize()
    local angle = -Math:ATan2(dir.y,-dir.x) + self.entity:GetCharacterControllerAngle() + 180.0
    self.entity:SetInput(angle,self.speed)
end

function Script:SetMode(mode)
    if mode~=self.mode then
        local prevmode=self.mode
        self.mode=mode
        if mode=="idle" then
            self.target=nil
            self.entity:PlayAnimation("Idle",0.01)
            self.entity:Stop()--stop following anything
        elseif mode=="roam" then
            if self.target~=nil then
                self.entity:PlayAnimation("Run",self.animspeedrun)
                self.entity:GoToPoint(self.target:GetPosition(true),5,5)
            else
                self:SetMode("idle")
            end
        elseif mode=="attack" then
            self:EndAttack()
        elseif mode=="chase" then
            if self.entity:Follow(self.target.entity,self.speed,self.maxaccel) then
                if prevmode~="chase" then
                    if self.sound.alert then self.entity:EmitSound(self.sound.alert) end
                end
                self.followingtarget=true
                self.entity:PlayAnimation("Run",self.animspeedrun,300)
                if self:DistanceToTarget()<self.attackrange*2 then
                    self.followingtarget=false
                    self.entity:Stop()
                    self:DirectMoveToTarget()
                end
            else
                self.target=nil
                self:SetMode("idle")
                return
            end
        elseif mode=="dying" then
            self.entity:Stop()
            self.entity:PlayAnimation("Death",self.animspeedDeath,300,1,"EndDeath")

        elseif mode=="dead" then
            if self.mode == "dead" then
                self.entity:SetCollisionType(0)
                self.entity:SetShape(nil)
                self.entity:SetPhysicsMode(Entity.RigidBodyPhysics)
                self.enabled=false
            end
        end
    end
end

function Script:EndAttack()
    if self.mode=="attack" then    
        if self.target==nil then
            self:SetMode("idle")
            return
        end
        if self.target.health<=0 then
            self:SetMode("idle")
            return
        end
        local d = self:DistanceToTarget()
        if d>self.attackrange then
            self:SetMode("chase")
            return
        end
        self.entity:Stop()
        self.attackmode = 1-self.attackmode--switch between right and left attack modes    
        self.entity:PlayAnimation("Attack"..tostring(1+self.attackmode),0.07,500,1,"EndAttack")
        self.attackbegan = Time:GetCurrent()
        if self.sound.attack[self.attackmode+1] then
            if math.random()>0.75 then
                self.entity:EmitSound(self.sound.attack[self.attackmode+1])
            end
        end
    end
end

function Script:UpdatePhysics()
    if self.enabled==false then return end
    
    local t = Time:GetCurrent()
    self.entity:SetInput(self.entity:GetRotation().y,0)
    
    if self.sound.idle then
        if t-self.lastidlesoundtime>0 then
            self.lastidlesoundtime=t+20000*Math:Random(0.75,1.25)
            self.entity:EmitSound(self.sound.idle,20)
        end
    end
    
    if self.mode=="idle" then
        if t-self.lastupdatetargettime>250 then
            self.lastupdatetargettime=t
            self.target = self:ChooseTarget()
            if self.target then
                self:SetMode("chase")
            end
        end
    elseif self.mode=="roam" then
        if self.entity:GetDistance(self.target)<1 then
            self:SetMode("idle")
        end
    elseif self.mode=="chase" then
        if self.target.health<=0 then
            self:SetMode("idle")
            return
        end
        if self:TargetInRange() then
            self:SetMode("attack")
        elseif self:DistanceToTarget()<self.attackrange*2 then
            self.followingtarget=false
            self.entity:Stop()
            self:DirectMoveToTarget()
        else
            if self.followingtarget==false then
                if self.entity:Follow(self.target.entity,self.speed,self.maxaccel) then
                    self:SetMode("idle")
                end
            end
        end
    elseif self.mode=="attack" then
        if self.attackbegan~=nil then
            if t-self.attackbegan>self.attackdelay then
                if self.target.entity:GetDistance(self.entity)<1.5 then
                    self.attackbegan=nil
                    self.target:Hurt(self.damage)
                end
            end
        end
        local pos = self.entity:GetPosition()
        local targetpos = self.target.entity:GetPosition()
        local dx=targetpos.x-pos.x
        local dz=targetpos.z-pos.z
        if self.entity:GetCharacterControllerAngle()>90.0 then
            self.entity:AlignToVector(-dx,0,-dz)
        else
            self.entity:AlignToVector(dx,0,dz)
        end
    end
end

function Script:UpdateWorld()
    if self.enabled == true then
        self.despawnTimer = self.despawnTimer + Time:GetSpeed()/100
        if self.despawnTimer > self.despawnTime then
            self.mode = "dead"
            self.entity:Hide()
            self.script = nil
        end
    end

    if self.mode == "dead" then
        self.removeBodyTimer = self.removeBodyTimer + (Time:GetSpeed()/100)
        if (self.removeBodyTimer > self.removeBodyTime) then
            self.entity:Hide()
            if self.entity:Hide() then
                self.entity:Release()
                self.script = nil
            end
        end
    end
end

function Script:DropLoot()

    self.inventory = {}
    
    self.inventory[1] = "HUD Elements/Inventory/Prefabs/Health.pfb"
    self.inventory[2] = "HUD Elements/Inventory/Prefabs/Stamina.pfb"
    self.inventory[3] = "HUD Elements/Inventory/Prefabs/Shield.pfb"
    self.inventory[4] = "HUD Elements/Inventory/Prefabs/Health.pfb"
    self.inventory[5] = "HUD Elements/Inventory/Prefabs/Stamina.pfb"
    self.inventory[6] = "HUD Elements/Inventory/Prefabs/Shield.pfb"

    self.gun = {}

    self.gun[1] = "AddOns/FPS Weapons Pack/pickup mp5.pfb"
    self.gun[2] = "AddOns/FPS Weapons Pack/pickup pistol.pfb"
    self.gun[3] = "AddOns/FPS Weapons Pack/pickup shotgun.pfb"
    self.gun[4] = "AddOns/FPS Weapons Pack/pickup m4.pfb"
    self.gun[5] = "AddOns/FPS Weapons Pack/pickup machete.pfb"

    self.ammo = {}

    self.ammo[1] = "Prefabs/Ammo Prefabs/9 ammo Hover.pfb"
    self.ammo[2] = "Prefabs/Ammo Prefabs/Combat Rifle Hover (5).pfb"
    self.ammo[3] = "Prefabs/Ammo Prefabs/mp5 ammo Hover (4).pfb"
    self.ammo[4] = "Prefabs/Ammo Prefabs/Shotgun Shells  Hover(3).pfb"

    math.randomseed(Time:Millisecs())
    num = math.random(1, 100)

    if num >= 0 and num < 15 then
        local spawn = Prefab:Load(self.inventory[math.random(1,6)])
        local SpawnPos = self.entity:GetPosition() + Vec3(0,1.5,0)
        spawn:SetPosition(SpawnPos)
        return
    elseif num >= 15 and num <= 20 then
        local spawn = Prefab:Load(self.gun[math.random(1,5)])
        local SpawnPos = self.entity:GetPosition() + Vec3(0,1.5,0)
        spawn:SetPosition(SpawnPos)
        return
    elseif num > 20 and num <= 40 then
        local spawn = Prefab:Load(self.ammo[math.random(1,4)])
        local SpawnPos = self.entity:GetPosition() + Vec3(0,1.5,0)
        spawn:SetPosition(SpawnPos)
        return
    end
end

Link to comment
Share on other sites

I figured out that if I raise the event under within the health > 0 statement I don't get that error, but it still doesn't reach the onAttacked function, as the System:Print() doesn't do anything.

if self.health>0 then
        if self.target==nil then
            RaiseEvent("onAttacked", { hurtMonsterPosition = self.entity:GetPosition(), player = distributorOfPain})
            self.target=distributorOfPain
            self:SetMode("attack")
        end

 

Having it like this seemed to let things run smoothly and then for no reason eventually I would get that error. But that appears to be only when I shoot an enemy from so far away that the pick isn't grabbing their name (and health). Since I put the enemy name in the onDead loop maybe that's giving me that error?

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...