Jump to content

Rick

Members
  • Posts

    7,936
  • Joined

  • Last visited

Posts posted by Rick

  1. 2 hours ago, SteamingIce said:

    The main thing people worry about (most people anyway) when they buy an engine, is the coding language. One issue I have had even with the most popular engines is the fact that I am proficient in C++ and C# yet 10/10 times I am picking between the two. Imagine if an engine had support for more languages? Theyd be the easy pick. I cant count how many times I have been coding in UE4 and said FK I wish they accepted C#, or Unity and C++. 

    I am unsure exactly HOW difficult it would be, but adding C# support would probably give you guys more traction as the more languages you are compatible with, the more people will choose your engine over others. Just a thought to run by you guys in this day and age of free programs.

    While more languages can attract more users it also adds overhead for the creators of the engine. It becomes just another thing to spend time on and Josh would have to determine if it's worth it. If it doesn't drive enough customers to makeup for the time spent on it is it really worth it? Someone who knows C# inside and out might already know C++ or willing to learn it since today they are getting more and more similar given C++'s advancements. So it might not scare those people away to not have C#. 

    • Like 1
  2. Remember this is being called on ALL the monsters. So hurtMonsterPosition is the monster that got hit, but when it fires for the other monsters self.entity is that other monster checking it's position against the one monster that got hit.

     

    A good thing you can do is set these values to a variable and System:Print() the value out so you can see what it is and if it's something that you aren't expecting. 

    • Like 1
  3. So when you're testing this stuff you can put a bunch of System:Print() commands in places to see if it goes into if statements and such. SO place a System:Print("A") inside the if data.hurtMonsterPosition blah blah to see if it goes inside that. If it does then you need to figure out how to set the player as this monsters target. I'm not 100% sure yet how to do it. I assumed it was just setting self.target but maybe not.

  4. OK figured it out. My loop in RaiseEvent() was wrong. Use this RaiseEvent() function and it works:

     

    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 k, v in pairs(events[eventName]) do
    		System:Print("Raising event "..eventName)
    		local scriptFunc = v.scriptFunction
    		local script = v.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

    The difference is the table of callbacks within a function doesn't go from 1 to x like how I was iterating over it. This loop will loop over all sub tables of the event no matter what eventId is there. My bad.

  5. I just use the old animation lua script Josh had because I built this idea into it and working with callbacks in Lua is just easier. Still wish by default this LE PlayAnimation function gave us that capability, or he added a RegisterAnimationEvent() function to link the timing to a function.

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

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

     

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

  9. 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! :)

     

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

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

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

     

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

     

  14. 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
  15. 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
  16. 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.

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

  18. 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
×
×
  • Create New...