havenphillip Posted June 9, 2019 Share Posted June 9, 2019 I have a camera pick I'm using the pickInfo.entity mode to signal the counter to add a kill but it doesn't stop at 1. I tried a table but it just reiterates the table until the camera moves away from the entity. What's the best way to count just 1 kill per enemy? This is my current: function Script:UpdateWorld() local pickInfo = PickInfo() self.proj = window:GetMousePosition() if self.camera:Pick(self.proj.x, self.proj.y, pickInfo, self.pickradius, true,2) then if pickInfo.entity:GetKeyValue("type") == "enemy" then if pickInfo.entity.script.mode ~= "dying" and pickInfo.entity.script.mode ~= "dead" then self.camPick = true elseif pickInfo.entity.script.mode == "dying" then --this is the problem. But what do I reference that will count just once? for i = 1,1,1 do self.kills = self.kills + i System:Print(i) end end end else self.camPick = false end end Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 Do you want to pick on mouse button down only? If so you'll need to add that function call around your camera pick call as this is picking every frame. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 I don't think I want that? I want it to display the enemy health when I'm looking at it. Ideally what I would like to do is pick the entity and somehow save that information so I don't have to keep picking it in order to count the kill. I got it to count 1 kill if I change the collision type on ...mode = "dying" but it will sometimes miss because I moved the camera. function Script:UpdateWorld() local pickInfo = PickInfo() self.proj = window:GetMousePosition() if self.camera:Pick(self.proj.x, self.proj.y, pickInfo, self.pickradius, true,2) then if pickInfo.entity:GetKeyValue("type") == "enemy" then if pickInfo.entity.script.mode ~= "dying" and pickInfo.entity.script.mode ~= "dead" then self.camPick = true else pickInfo.entity:SetCollisionType(Collision.None,false) <-- changed these two lines here pickInfo.entity:SetMass(0) for i = 1,1,1 do self.kills = self.kills + i System:Print(i) end end end else self.camPick = false end end This is what I'm doing with it: Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 What does "count the kill" mean? You first talk about just showing it's health then "count the kill"? Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Yeah the health is really beside the point. I got the pick working. It's counting the kill that I'm having a problem with. It only counts the kill currently if I keep picking the crawler after health is <= 0. So if I look away too soon I don't get the kill point, which is lame. You can see in the top left corner I'm trying to count cumulative crawlers killed. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 Normally if you want to show the health like this you do it on every pick like you are. If you want to shoot it you do another pick inside a mouse down (assuming left click mouse down shoots) to check what you shot and apply dmg to that thing. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 I'm able to shoot it and kill it. I just can't count it. Are you saying I need to make another pick on mouse down only in order to count it? Like if health <=0 then kill = kill +1, but within the other pick? Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 Where is your code for shooting it? After you shoot it is where you should check if health is 0 and then count the kill there at the moment of when it's shot. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Just the FPS script. This, I think: --Raycast Pick that is being send from the camera in to the world self.canUse = false local fire = false local currentime = Time:GetCurrent() if self.carryingEntity==nil then if self.weapons[self.currentweaponindex]~=nil then if self.weapons[self.currentweaponindex].automatic then if window:MouseDown(1) then fire=true else self.suspendfire=false <-- so like in here somewhere? like a pickInfo.entity.script.health something? end else if window:MouseHit(1) then fire=true end end end end Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 That looks like it's just setting a fire variable. Somewhere else is where it's applying the damage. Have to find that. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Ah ok. Looks like it's in the FPSGun script. function Script:UpdateWorld() local bullet,n,dist local pickinfo=PickInfo() local firstbullet=true local travel for n,bullet in ipairs(self.bullets) do --Check how far the bullet has travelled dist = (bullet.position-bullet.origin):Length() if dist>self.bulletrange then table.remove(self.bullets,n) bullet.sprite:Release() bullet=nil end if bullet~=nil then travel = bullet.velocity/60.0*Time:GetSpeed() if self.entity.world:Pick(bullet.position,bullet.position+travel,pickinfo,0,true,Collision.Projectile) then --Find first parent with the Hurt() function local enemy = self:FindScriptedParent(pickinfo.entity,"Hurt") <--- this guy right here. --local sph = Model:Sphere() --sph:SetPosition(pickinfo.position) Could I just put the kill counter in the crawler script under the "Hurt" function then? Since it's telling it to read that here? Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 Show me more of that script. I assume a little further down it's calling enemy:Hurt()? Does it have the player somewhere (the owner of this gun). After calling enemy.Hurt() is where I would check the health of the enemy and if zero add one to the kill counter on the player as that's the point as to where you've killed an enemy and I assume the player is the thing that holds the kills variable? Quote Link to comment Share on other sites More sharing options...
Thirsty Panther Posted June 10, 2019 Share Posted June 10, 2019 If you are using the default Monster AI script then the part you are looking for is: 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 if self.health<=0 then self.entity:SetMass(0) self.entity:SetCollisionType(0) self.entity:SetPhysicsMode(Entity.RigidBodyPhysics) self:SetMode("dying") end end end After the self:SetMode("dying") place your code to increase the number of kills. Kills= Kills + 1 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Yeah here's the whole function. You're right. Later it calls: if enemy.script.health>0 then enemy.script:Hurt(self.bulletdamage,self.player) end ...so like if enemy.script.health>0 then enemy.script:Hurt(self.bulletdamage,self.player) if enemy.script.health <=0 then self.player.script.kills = self.player.script.kills + 1 end end function Script:UpdateWorld() local bullet,n,dist local pickinfo=PickInfo() local firstbullet=true local travel for n,bullet in ipairs(self.bullets) do --Check how far the bullet has travelled dist = (bullet.position-bullet.origin):Length() if dist>self.bulletrange then table.remove(self.bullets,n) bullet.sprite:Release() bullet=nil end if bullet~=nil then travel = bullet.velocity/60.0*Time:GetSpeed() if self.entity.world:Pick(bullet.position,bullet.position+travel,pickinfo,0,true,Collision.Projectile) then --Find first parent with the Hurt() function local enemy = self:FindScriptedParent(pickinfo.entity,"Hurt") --local sph = Model:Sphere() --sph:SetPosition(pickinfo.position) --Bullet mark decal local mtl local scale = 0.1 if enemy~=nil then mtl = Material:Load("Materials/Decals/wound.mat") scale = 0.1 else if pickinfo.surface~=nil then local pickedmaterial = pickinfo.surface:GetMaterial() if pickedmaterial~=nil then rendermode = pickedmaterial:GetDecalMode() end end mtl = Material:Load("Materials/Decals/bulletmark.mat") end local decal = Decal:Create(mtl) decal:AlignToVector(pickinfo.normal,2) decal:Turn(0,0,Math:Random(0,360)) decal:SetScript("Scripts/Objects/Effects/BulletMark.lua") if mtl~=nil then mtl:Release() end decal:SetPosition(pickinfo.position) decal:SetParent(pickinfo.entity) --Apply global scaling local mat = decal:GetMatrix() mat[0] = mat[0]:Normalize() * scale mat[1] = mat[1]:Normalize() * scale mat[2] = mat[2]:Normalize() * scale decal:SetMatrix(mat) table.remove(self.bullets,n) bullet.sprite:Release() if enemy~=nil then if enemy.script.health>0 then enemy.script:Hurt(self.bulletdamage,self.player) end --Blood emitter --[[e = self.emitter[2]:Instance() e = tolua.cast(e,"Emitter") e:Show() e:SetLoopMode(false,true) e:SetPosition(pickinfo.position+pickinfo.normal*0.1) e:SetVelocity(0,0,0)]]-- else --Add a temporary particle emitter for bullet effects local e e = self.emitter[0]:Instance() e = tolua.cast(e,"Emitter") e:Show() e:SetLoopMode(false,true) e:SetPosition(pickinfo.position) local v=3 e:SetVelocity(pickinfo.normal.x*v,pickinfo.normal.y*v,pickinfo.normal.z*v,0) --Smoke emitter e = self.emitter[1]:Instance() e = tolua.cast(e,"Emitter") e:Show() e:SetLoopMode(false,true) e:SetPosition(pickinfo.position+pickinfo.normal*0.1) local v=0.2 e:SetVelocity(pickinfo.normal.x*v,pickinfo.normal.y*v,pickinfo.normal.z*v,0) --Play bullet impact noise e:EmitSound(self.sound.ricochet[math.random(#self.sound.ricochet)],30) if pickinfo.entity~=nil then --Add impulse to the hit object if pickinfo.entity:GetMass()>0 then --local force = pickinfo.normal*-1*self.bulletforce local dir = bullet.velocity:Normalize() local force = dir * self.bulletforce * math.max(0,-pickinfo.normal:Dot(dir)) --force = force * math.max(0,-pickinfo.normal:Dot(d))--multiply by dot product of velocity and collided normal, to weaken glancing blows pickinfo.entity:AddPointForce(force,pickinfo.position) end --Extract a partial surface from the hit surface and make a bullet mark --To be added later --if pickinfo.surface~=nil then -- local aabb = AABB(pickinfo.position-radius,pickinfo.position+radius) -- local surf = pickinfo.surface:Extract(aabb) --end end end else bullet.position = bullet.position+travel bullet.sprite:SetPosition(bullet.position - bullet.velocity:Normalize()*1) if bullet.sprite:Hidden() then dist = (bullet.position-bullet.origin):Length() if dist>bullet.sprite:GetSize().y then bullet.sprite:Show() end end end end firstbullet = false end end Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Count the kills in the Hurt function of the AI? Or count it in the player script? I'm currently attempting to count them from a pivot to the player. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 I'd just put the kill variable on the player. What you have there I think would work. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Ok, thanks. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 Ah that works perfectly. It was as simple as putting... "if enemy.script.health <=0 then self.player.kills = self.player.kills + 1 end" ...in the FPSGun script and "Script.kills = 0" in the player script. And then drawing the text also in the player script. 2 Quote Link to comment Share on other sites More sharing options...
Rick Posted June 10, 2019 Share Posted June 10, 2019 Glad you got it working. Now this is fine and all and it clearly works. That being said thinking about a higher level architecture with this stuff it's probably not idea that it's in a gun script. You might end up with different gun scripts based on the gun type later in your game and you'd hate to duplicate this code in each gun script. You wouldn't want to put it in the enemy Hurt() function (which you could because you do have access to the player inside that function) because you may have different enemy scripts later in your game (because different enemies act different ways and they'd need their own script to do that functionality) and again you'd hate to duplicate this logic in all different enemy scripts you may have. Anytime you duplicate code you're opening yourself up for bugs. Imagine you forget to put this logic in some different enemy script and now your users complain that enemy X doesn't count towards their kills. These are things to think about. A pie in the sky system would probably have enemies raise some sort of onDead event that anything can hook into. Then in any enemy script when they die you just raise that event. Yes, you have to remember to raise that event in each different enemy script you may have but it's a much more generic thing (dying) than remembering to increment a kill counter inside each different enemy script and less likely to forget. Today you are just increasing a kill counter but you may soon find you need to do a bunch of different stuff when an enemy dies. If you had some sort of event when an enemy dies your player could hook into that event and have a player script function called when an enemy dies and then you can do all your stuff inside the player script (a place where other programmers would probably expect to see this stuff anyway). This also gets into other architecture ideas like right now you're tightly tied your enemy script to your player script because it's expecting the player script to have a kills variable. If you try to reuse this enemy script in a different game but don't need a kill count in that game, this piece of logic would blow up. This is what is known as tightly coupled. You've coupled your 2 scripts together. Ideally you try to avoid doing that as much as possible. Again, this works and is fine until things get more complex and it's not fine The joys are software architecture! 2 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 10, 2019 Author Share Posted June 10, 2019 That's rad, dude. That's definitely the level I want to get to. I want to be able to think in terms of whole systems like that but I just don't know what steps to take to get there. I'm still a total noob so pretty much everything is uphill now. It's getting easier but it all tends to get cluttered in my head and I can't remember where I put things, etc. What do you recommend I study next to get to the next level? I have Aggror's FlowGUI. I was thinking of getting into that to see if I can understand his thinking behind it. I like that it's a whole system but maybe its a little too big for me at this point. What do you mean by "onDead"? Like a single function that takes care of everything? Or a single script? I can't even grasp how I would uncouple this lol Quote Link to comment Share on other sites More sharing options...
Rick Posted June 11, 2019 Share Posted June 11, 2019 An event system can be a good way to deal with communication between different game objects. Multiple objects can listen for a certain event and another object emits the event. If I was to do a basic one in LE I'd create a global table called events. Then I'd create global functions called RaiseEvent(eventName, data), SubscribeEvent(eventName, script, scriptFunction), and Unsubscribe(eventName, subId). An event is just a string name. You can call RaiseEvent(stringEventName) anywhere. If any entity subscribed to that event a function they defined will be called. You can also have many different entities subscribed to the same event so may entities will be informed when it's raised. I'm doing this on the fly but let's see if we can get it to work There may be typos to work through but a basic system like this is a good start to decoupling your game entities. Place the following code in the main lua script. events = {} subId = 0 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][i].scriptFunction local script = events[EventName][i].scriptObject -- call the script function sending the data as well -- when you call a script function, the first parameter is the script itself, lua hides this parameter in the function itself and assigns it to self -- this is why inside Leadwerk script functions defined like Script:myFunction() you can use self. inside of it. So in this case your function you -- hooked this to will only have 1 parameter which is the data -- data in this case is anything you want it to be when you raise the event. the subscribers to the event will need to understand what data to expect scriptFunc(script, data) end end Usage: Inside enemy script Hurt() function check if health <= 0 and if it is call: RaiseEvent("onDead", {}) For now you can have the data parameter be an empty table since you don't care about anything but if they died, but later you may care about some data about who died. You can fill that inside the data table at a later date. Inside your player script: function Script:Start() -- we want to subscribe to the onDead event (events are really just string names of whatever I want to call an event. when any object calls RaiseEvent("onDead") it'll call my self.enemyDied function so I know about it! self.onDeadId = SubscribeEvent("onDead", self, self.enemyDied) end function Script:enemyDied(data) self.kills = self.kills + 1 end function Script:CleanUp() Unsubscribe(self.onDeadId) end What's really interesting about this is that turning these subscribed functions into coroutines is pretty simple and that gives you the ability to do things over time inside your subscribed functions. A lot of game stuff is done over time with animations and such and coroutines provides a nice easy way to manage that stuff. If I have time this month I may create a library for this stuff so it's easy for people to use. 1 Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 Ok so let me see if I'm following you here. I put that first code in my Main.lua at the top under "import("Scripts/Menu.lua")". I hid "self.kills = self.kills + 1" in the FPSGun script. I put " RaiseEvent("onDead", {}) " in the crawler script under the Hurt() below "if self.health <= 0 then" I put "self.onDeadId = SubscribeEvent("onDead", self, self.enemyDied)" in the Start() of the player. I put ... "function Script:enemyDied(data) self.kills = self.kills + 1 end" and... "function Script:CleanUp() Unsubscribe(self.onDeadId) end" ... in the player script. Everything just like you wrote it. I'm getting no errors but it's also not counting kills. Is it supposed to work as-is? I re-watched your vids on the SMC hoping some light would go on but my brain just won't retain it. Quote Link to comment Share on other sites More sharing options...
Rick Posted June 12, 2019 Share Posted June 12, 2019 There is a typo in RaiseEvent() and Unsubscribe(). The parameter name is 'eventName' but inside I'm using 'EventName' (capital E instead of lower). Fix that in all places in those 2 functions and it works. I'm actually very shocked this was the only error. I just did this in notepad at work from memory lol. So now you have a more generic way to communicate between game objects without them having to know about each other's script/entity information. If you had a UI script/entity where you did all your UI stuff you could actually move the kill counter to that and have it listen to this event and update the UI as perhaps the player script storing kill counts isn't ideal. So now you can pass around all sorts of events. Let's say your UI script needs to end the game after 10 kills. When that kill counter hits 10 raise another event like "onEndRound" and have the player listen and act accordingly (maybe don't allow movement), and same for the Monster script so they stop. I'll work on getting these script functions that you subscribe to being coroutines. The usage of all this stuff would stay exactly the same but inside these event subbed functions you could call coroutine.yield() which will get out of the function at that point for 1 frame and then get back into the function at exactly that same point the next frame. This would allow you to do some stuff over multiple frames in 1 function in a loop. Normally you couldn't do that as the loop would execute all in 1 frame and you'd see the final result, but since coroutines leave the function on the yield() call and does a complete game loop iteration and then comes back in at that same point the results of what you do inside the loop is visible. 1 Quote Link to comment Share on other sites More sharing options...
Rick Posted June 12, 2019 Share Posted June 12, 2019 OK, got the coroutine stuff in and it seems to work. I'm just going to show all code. Inside Main.lua (eventually you might want to pull this into it's own file and import it into Main.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][i].scriptFunction local script = events[eventName][i].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 I added a WaitForSeconds() as a utility function to show how coroutine.yield() works inside these event subbed functions. This helps abstract functionality for this coroutine stuff. You can create a bunch of other utility functions for like WaitForSound(snd) which could wait for the sound to finish playing before continuing on, etc. Inside Main.lua between Time:Update() and world:Update() put (note I just put Time:Update() and world:Update() to show where. Don't add those again) --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[i].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[i].co, eventCoroutines[i].script, eventCoroutines[i].args) end end --Update the world world:Update() As a test inside Player script: function Script:enemyDied(data) WaitForSeconds(2.5); System:Print("Enemy died") WaitForSeconds(1.0); System:Print("Wow this is cool!") end So when you kill the enemy 2.5 seconds will pass then in the console you'll see "Enemy Died" then 1 second will pass an you'll see "Wow this is cool!". Now in this particular case you can just set your kill count just like normal, but just think about some cases where you may want to loop over something but show the results on screen while looping. Quote Link to comment Share on other sites More sharing options...
havenphillip Posted June 12, 2019 Author Share Posted June 12, 2019 Ok. I did that. I'm not getting anything, though. Put the script in Main. Didn't duplicate the Time or World updates. Added the enemyDied function in the player script. Should this line : "self.onDeadId = SubscribeEvent("onDead",self,self.enemyDied)" ..be like this? "self.onDeadId = SubscribeEvent("onDead", self, self:enemyDied())" I tried it like that and it starts me off with one kill. And then doesn't count any of the kills. You got it working so what am I missing here? Also how do I move it to a different script? Just put it on a different script and then at the top of Main.lua add "import"..scriptname...""? This would be awesome for making a little "kill list" on the screen. I got my little random name generator working on the Crawlers, so I could make a list like "Gorgon was killed by player" etc. 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.