Jump to content

Rick

Members
  • Posts

    7,936
  • Joined

  • Last visited

Everything posted by Rick

  1. OK, so it's something in the setup we're missing. Can you just attach the entire monster script file to this post?
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. What's your gfx card? Perhaps it's not powerful enough to run LE? Look in Documents\Leadwerks and copy/paste Leadwerks.log file here so we can see what it produced.
  7. 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
  8. 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!
  9. 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. 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.
  10. 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
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. Input cheats like that is one thing and is harder for the other player to really even know is happening. If a game like that was peer to peer then they could be hitting my character and my health isn't going down at all. That's a cheat they would know instantly and get very frustrated about and stop playing. We're really talking about network cheating where input cheating like that is a different topic I think.
  16. Peer to peer encryption doesn't work. The clients has the code for decryption then in that case in order to read it which means anyone can see how to decrypt the packets. There is no way to hide any keys or anything in peer to peer. Many games have been killed (even non e-sport games) by cheating. Gamers don't put up with that at all. "L4D and other big games work fine with this system." L4D is a co-op game where cheating doesn't really matter as it's players vs AI, although if you were in a game where someone did cheat the AI you'd be pretty annoyed and if it happened most of the time you'd simply stop playing the game. Any game that is player vs player cheating will kill your game. So peer to peer is useful in very specific game types and in others cheating will run rampant and destroy the game (if enough people care about the game). I've been thinking about trying a Clash Royale (https://play.google.com/store/apps/details?id=com.supercell.clashroyale&hl=en_US) type of game and even in a game like this that is fairly simple to implement as the player is really just placing pieces and then everything runs on the server from there, a game like this would have a ton of cheating if it was peer to peer as the host gamer would be able to control everything and easily cheat to win. There aren't that many games out there that can survive peer to peer networking.
  17. 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!
  18. I'd just put the kill variable on the player. What you have there I think would work.
  19. 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?
  20. That looks like it's just setting a fire variable. Somewhere else is where it's applying the damage. Have to find that.
  21. 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.
  22. 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.
  23. What does "count the kill" mean? You first talk about just showing it's health then "count the kill"?
  24. 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.
  25. Given this is a post about multiplayer the cheating would be on the network commands they can send or suppress if they are the host of the game (for games that have no central server).
×
×
  • Create New...