Jump to content

Best way to count kills?


havenphillip
 Share

Recommended Posts

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

Link to comment
Share on other sites

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:

 

crawler.png

Link to comment
Share on other sites

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.

Link to comment
Share on other sites

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.

Link to comment
Share on other sites

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

Link to comment
Share on other sites

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?
               

Link to comment
Share on other sites

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?

Link to comment
Share on other sites

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

 

Link to comment
Share on other sites

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

Link to comment
Share on other sites

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.

  • Like 2
Link to comment
Share on other sites

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!

  • Like 2
Link to comment
Share on other sites

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

Link to comment
Share on other sites

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. 

  • Like 1
Link to comment
Share on other sites

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.

Link to comment
Share on other sites

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.

 

  • Like 1
Link to comment
Share on other sites

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.

 

Link to comment
Share on other sites

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.

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...