playMaker

Author Topic: Projectile & Health System: Best Practices?  (Read 3946 times)

Thore

  • Playmaker Newbie
  • *
  • Posts: 47
    • View Profile
Projectile & Health System: Best Practices?
« on: April 06, 2017, 08:46:47 AM »
It's going to be a Wall of Text.

I've built a few setups with projectiles and health managers, and seen even more tutorials how others are doing it. I'm somewhat unsure what are the best methods (performance, maintenance, ease of use etc), as different people have very different approaches but not much reasoning why exactly.

So I'm offering some ramblings and thoughs about how I do it, and why (reflecting my knowledge/ignorance at this point). The benefit might be a sanity check for everyone, what works and what doesn't and why. :)

1. Modular: I try to keep the parts modular. As such, the weapon-firing logic sits on the weapon (in different FSMs, one for firing, one for cooldown/reload functions). It just waits for a triggering event to fire. This then creates a projectile. Likewise, the visual representation of the player and enemies is a "skeleton" object I can just child underneath a holder with the FSMs. Same for health. This might be over the top for your game, but mine relies on re-using parts with different data. Most tutorials put the attacking logic into the holder, in contrast to my method where the weapon does the thing. However, the pitfall is that you need to think clearly what is horse and what is cart at any point.

2. Not Pooling:  I don't do pooling at the moment. I worry about this a bit. Frequent recommendation is to not create and destroy objects, but instead disable, and transform them (teleport them) and reuse them. I don't know how to do this well (arrays?). I tried this with dust particles on the running animation, but the copying and pasting of transform vectors, accessing the objects and switching them on and off seemed not much better (arguably worse). If someone is bored, an action similar to create, but draws from pool would be awesome. Likewise a complementary action that puts back to pool instead of destroy would be neat. I could have placed the dust into the running animation (switching it on and off there), but for now, it's just a particle "module" (see point 1) that is switched on and off per FSM on every step. I anyway needed an FSM for the footstep sound. I also have simple script for that, but for now opted to keep things in Playmaker. I noticed that on rare occasions the dust was briefly appearing on the zero vector 0,0,0 on enable instead off on under the feet where I placed it. Clearly, there are better ways to do this, but for now this is good enough. However, for projectiles I use old fashioned create/destroy projectiles. On hit, the projectile creates another object for the destroy particles. I can also potentially add area of effect type of stuf there.



3. Projectile Knowledge: The projectile first gets its essential data from the weapon and the player, such as who is enemy team, amount of damage it does etc. For determining stuff it can hit, I have dynamic (variable) and static tags ("hardcoded"). The static ones are simply set into the trigger event from the tag list. The dynamic tag is a variable and will hit whatever string is placed inside it. The purpose is that the projectiles from Team 1 need to look for "Team 2" and vice versa.

4. Know Your Enemy: "Tags" are too limiting, but for now they seem the best way to handle the world markup. Markup is basically the semantics or meaning of the game world, so that the game "knows" what is enemy, wall, ground and so forth (which can also be relative, e.g. my allies are my enemies enemies). The problem is that sometimes projectiles need to know quite a lot. I currently use "Trigger Event" multiple times in a list, to check for various tags I want being affected. Imagine you have three teams, and no friendly fire. Each needs to damage only the other two team tag holders (this is for the sake of illustration). In such a case, there need to be two Trigger Events, one looking for "Team 2" and one for "Team 3" (when the owner is "Team 1"). Is there's a better way, or some trick to expand tags so that objects can have multiple ones? Ideal would be hierarchy of tags, so that you only look for "stuff that can be affected" vs "decoration" and only once it's found, proceed from there. My workaround, to be done, will be a general "stuff that can be hit" tag, and then additional checks for what to do in each case. However, this might have subtle design implications.

5. Time Based Range: The Trigger Events send the "hit" event, which gets propagated to the target's Health module if it finds one. Underneath this Trigger Event list in the same state sits a wait action. If no "hit" event was fired, the wait will fire a "miss" event. The wait time determines the projectile range, naturally. This skips the hit computation and goes straight to the final state containing "destroy self". Time based range is not super exact, but works totally fine for my purposes. I guess an actual range computation would be some nightmarish checking of vectors.

6. Not Using RunFSMs: If one of the Trigger Events finds a collider with the proper tag, the whole damage calculation starts. Different tutorials work with RunFSM, but I don't see a good reason for it. I use RunFSMs to apply status effects, because they are highly variable, and used temporarily -- which in my albeit limited understanding are the best reasons for RunFSMs. In other words, you don't want a dozen of status effect ("poisoned" etc) FSM sit dormant on every actor waiting for being activated. But basic hit points / health management is always the same, and hence I don't see a reason why it should be moved into a RunFSM.

7. Health: Hence, following my modular approach, I have a universal "health" FSM template (any modifications to it are always applied everywhere), and it sits on a health prefab object. I put this as child to anything I want to use a damage system on. Put it below a cube -- done! How this works is that the projectile, after having hit something, looks for this Health component, and passes on its damage data and also triggers a global event in that FSM (e.g. "HIT"). Following convention, I type global events in uppercase.

8. Metaphors & Variables: Conventions and game metaphors are important. Instead of calling it "damage", I call it "energy", and my convention is that damage is minus and healing is positive. If you call the variable "damage" and it's a positive number, i.e.  "10 damage" you might wind up in complications once you want healing. Also, you can conceptualize 1 points being a "quarter heart" (in terms of Zelda), or you can declare that 0.25 as float is one quarter heart. I use INTs, which I find cleaner and less error prone. It also frees you from confusing system with game metaphor. Should you later want to divide your hearts into 6 pieces, you don't have to throw away your balance, just adjust the representation. You still have e.g. 10 points, but they are now displayed as "one Lifeforce and 4 pieces". You might want to use floats when you really need to do complicated computations. I would advice to reconsider your game design, because ultimately players want to understand what "health" means. It is a central game metaphor and should not be opaque what goes on.

(9.) The Health object is initialized by setting the clamp to prevent healing going over the top. I want that healing is possible to up to about 20%, and clamp it there. With floats, you can just do *1.2. I do this by copying starting HP to HP Max, then temporarily compute 1/5th then add both HP and Max together to have the real Max; i.e. Looks cumbersome, but at least it keeps the numbers clear.

Step One 10HP = 10Max
Step Two 10Max/5 = 2Max
Step three 10HP + 2 Max = 12Max.
Done.



(10.) After HIT is triggered, it first checks whether we're invincible. I have this for now, and is intended to have a spawn protection. Invincible begins as true, and thus no energy can pass through (also no healing). A separate FSM on the same health object sets it to false after a short time. This is another frequent design pattern. If this was on the same FSM, triggering the global event (the "HIT") could interrupt the setup. I also use this pattern on the weapon for the cooldown/reload logic. The only thing they do is wait, and then flip some bool on the main FSM, thereby allowing the series of events sloshing through. It may be useful to properly initialize objects that have unpredictable global events in them. You would then always place the bool check underneath the global event, as a gate, and only if set properly, the event is being let through.

(11.) Computing the incoming energy is a simple Int Add. Since damage is negative, it substracts as intended (e.g. 10 Plus [-3 Energy] = 7). Directly afterwards, incoming energy is set to 0, of course, now that it was applied. Finally it's passed through a simple Int Clamp with the Hitpoint Max determined above. Finally, the rat tail of animations and updating GUI is handled (I do this elsewhere). This is also a good practice in my book, because it keeps the health component universal. It will just output "I have no HP anymore" at some point and characters might play death animations, walls might crumble etc.

(12.) Next, I check whether we're still alive. If Health is below 0, I move to Destroyed sequence of states. The first after that is a state with a simple bool check to prevent a potential bug. It asks whether we triggered Destroyed already (same idea as in 9). If we did, it goes into a dead end state of doing nothing. I use this for now to prevent a bug I had in some version, where Destroyed was interrupted over and over again by subsequent projectiles. I might be fixing this by immediately changing tags, but I leave this idea here, because this failsafe design pattern is useful in other circumstances. If Destroyed routine was not triggered yet, it continues where I next set the bool "destroyed triggered once" to true (i.e. one gets through and locks door behind itself). After that, I send events around and change the holder tag (so that projectiles no longer care about this object).

(13.) Final useful thing for me is colour coding of links and states in FSMs. Colouring links is a bit hidden (you find this under right click on an event). The most crucial to me is clearly showing (1) dependencies to other FSMs or other components (animator, scripts etc), and to indicate "Work in Progress/Debug" parts. It would be great if Playmaker would be showing outgoing events similar to the incoming ones (from global events), i.e. thicker black arrow coming out of a state. Even better, if there was an overview that shows FSMs as boxes with all the events as ins-and-outs showing the relationship between them. For now, I use a workaround (which might be a bad idea), basically "dead" (i.e. non-functional) states and events. I create an ordinary local event, e.g. "Send Events", which I put last on a state that sends stuff around. This event is then connected to an empty state named like the global event it triggers, e.g. "HIT". I put the target FSM name in the comment instead of the state name, because the same FSM can be targetted with different global events.  You see an example how this looks like in the screens I put above. Green is always receiving (green state) or sending stuff (green links to non-functional state), Red is debug/work in progress.

/applause for having read to the end! I greatly appreciate comments, ideas, anything you might have.

Rabagast

  • Hero Member
  • *****
  • Posts: 623
    • View Profile
    • Homepage
Re: Projectile & Health System: Best Practices?
« Reply #1 on: April 14, 2017, 02:43:35 AM »
Quote
3. Projectile Knowledge: The projectile first gets its essential data from the weapon and the player, such as who is enemy team, amount of damage it does etc. For determining stuff it can hit, I have dynamic (variable) and static tags ("hardcoded"). The static ones are simply set into the trigger event from the tag list. The dynamic tag is a variable and will hit whatever string is placed inside it. The purpose is that the projectiles from Team 1 need to look for "Team 2" and vice versa.

Hi!

I will not write that much. :)
Do you use trigger event or collision event on the projectile?
I use collision event and I recommend that if you need something to collide in highspeed. For example a bullet. I just tried a projectile with trigger event and a speed of 15. The projectile just passed through the target. I changed it to collision event and even if it has a speed of 1000, it will stop at the target and explode.

The projectile speed and collision, I have that on the FSM to the projectile. And then I store the collision in a variable, and send and event to that object it hits.

Quote
7. Health: Hence, following my modular approach, I have a universal "health" FSM template (any modifications to it are always applied everywhere), and it sits on a health prefab object. I put this as child to anything I want to use a damage system on. Put it below a cube -- done! How this works is that the projectile, after having hit something, looks for this Health component, and passes on its damage data and also triggers a global event in that FSM (e.g. "HIT"). Following convention, I type global events in uppercase.

I use Energy Bar Toolkit for this which is a very good asset. :)
Check out our homepage. http://www.walsberg.no
Or my personal game blog for news about my games. http://retro-tetro.walsberg.no

adamz

  • Playmaker Newbie
  • *
  • Posts: 1
    • View Profile
Re: Projectile & Health System: Best Practices?
« Reply #2 on: May 19, 2017, 11:15:13 PM »
Hi there,

Thanks for sharing all this! I was wondering if there's a way to check enemy health constantly (to determine whether enemy is dead or not) universally instead of checking it upon receiving damage?

I'm asking because I have a lot of damage sources in my project e.g. projectiles, melee, DOTs and it was wondering if there's a way to do this without having to add this logic in each damage source.

krmko

  • Sr. Member
  • ****
  • Posts: 422
    • View Profile
    • Fat Pug Studio
Re: Projectile & Health System: Best Practices?
« Reply #3 on: May 20, 2017, 12:49:01 AM »
Great post, i'll share some of my FSM's and experiences a bit later (i'm on cellphone now). In short, i used Core Game Kit, Unity tag system (so stupid multiple tags are not implemented yet) and a few FSM's to modify stuff a bit (different armor and bullet types that react differently to each other).

Rabagast

  • Hero Member
  • *****
  • Posts: 623
    • View Profile
    • Homepage
Re: Projectile & Health System: Best Practices?
« Reply #4 on: May 21, 2017, 09:07:34 AM »
Great post, i'll share some of my FSM's and experiences a bit later (i'm on cellphone now). In short, i used Core Game Kit, Unity tag system (so stupid multiple tags are not implemented yet) and a few FSM's to modify stuff a bit (different armor and bullet types that react differently to each other).

Hi!

This is what I do:
Each weapon has different damage power. The sword takes for example 15 HP, the axe 25 and so on. Each enemy has different HP, so you set the HP health on the enemies. Create a new Global Event and name it whatever you want. Taken Damage, Hit By Player, etc.

On the weapons, you use Trigger Event or Collision Event and store the enemy in a variable. So when you hit an enemy with a weapon, you send an event to that enemy you hit, using the variable. I use add FSM Float. Depends if you use Int or Float for the health. On the Add FSM Float (Int), you use the enemy variable, the FSM Name of the enemy and the HP Health variable. On Add Value, you use the HP Damage on the weapon, then send an Event to that FSM, using the Global Event you created. On the enemy you use a compare action, when the health reach 0 or lower, then you kill that enemy.

Hope this helps. :)
Check out our homepage. http://www.walsberg.no
Or my personal game blog for news about my games. http://retro-tetro.walsberg.no

Thore

  • Playmaker Newbie
  • *
  • Posts: 47
    • View Profile
Re: Projectile & Health System: Best Practices?
« Reply #5 on: July 21, 2017, 05:54:29 AM »
Hi there,

Thanks for sharing all this! I was wondering if there's a way to check enemy health constantly (to determine whether enemy is dead or not) universally instead of checking it upon receiving damage?

I'm asking because I have a lot of damage sources in my project e.g. projectiles, melee, DOTs and it was wondering if there's a way to do this without having to add this logic in each damage source.

I don't know if I understand your question correctly. It depends what exactly you are trying to solve.

You need to first establish what something like "dead" means in clear terms and in a technical sense. This isn't as trivial as it sounds. Health points below zero might be considered dead. You could always access the target's health point variable, and if this is zero, then you consider it as a dead and compute from there. But that's not a good way to go about it, because in a dynamic game, health might be healed a frame later again etc.

Rather, it's better to have a high level variable, like a bool "alive". Now dead means "alive = false". Next, when the enemy receives damage, and does its computation (like subtracting points) you should check there and then, whether the health is still good. Constant checking would be pointless, since the variable storing the health points is "constant" so to speak.

If the health points go below zero, you set alive to false, naturally. Now, your health FSM can either go a different route, and trigger the rat tail of stuff that needs to happen when something dies, like playing animations, sounds etc. and ultimately removing (or pooling) the game objects from the stage. You could tick off a different dedicated "death routine" FSM, if you need.

Now to solve your other problem, as part of the death sequence, you could immediately set the tag to something else that the colliders and triggers don't recognize. Back to "untagged" should suffice, but you can also introduce a "corpse" tag, and in a fancy RPG you can even use that further for a necromancer doing something with stuff that is tagged "corpse" etc.