Rick Posted June 12, 2019 Share Posted June 12, 2019 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. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 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 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 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 Quote Link to comment Share on other sites More sharing options...
Rick Posted June 12, 2019 Share Posted June 12, 2019 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 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 Ah ok! That's working. Still not getting anything in the System:Print() though. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 So that's like how in the video you put a function inside a function. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 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 Quote Link to comment Share on other sites More sharing options...
Rick Posted June 12, 2019 Share Posted June 12, 2019 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. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 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: Quote Link to comment Share on other sites More sharing options...
Rick Posted June 12, 2019 Share Posted June 12, 2019 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! Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 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 Quote Link to comment Share on other sites More sharing options...
Rick Posted June 13, 2019 Share Posted June 13, 2019 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 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 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. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 13, 2019 Share Posted June 13, 2019 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. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 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 Quote Link to comment Share on other sites More sharing options...
Rick Posted June 13, 2019 Share Posted June 13, 2019 Did you subscribe to that event in the Start() function? Remember you have to always subscribe to the event as well in order to receive it. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 Ah ok. Hold on let me see if I can figure that out. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 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. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 13, 2019 Share Posted June 13, 2019 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. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 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. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 13, 2019 Share Posted June 13, 2019 So first I'm curious if it's even calling the onAttacked function so comment out everything in that function and just put a System:Print() and print something to console and see if anything shows up. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 Yeah it's not even getting that far. I keep getting the same error. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 13, 2019 Share Posted June 13, 2019 OK, so it's something in the setup we're missing. Can you just attach the entire monster script file to this post? Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 13, 2019 Author Share Posted June 13, 2019 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 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 14, 2019 Author Share Posted June 14, 2019 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? Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.