Entitas icon indicating copy to clipboard operation
Entitas copied to clipboard

[Design] Integrating animation sequences together with damage calculation in Entitas (RPG battle system)

Open AVAVT opened this issue 5 years ago • 4 comments

Hi,

Animation and Entitas is an issue I had for a long time, and still couldn't find a definite solution for it. There are several use cases that differ slightly, so I thought I should make an issue instead of asking in the chat.

Most of the problems I couldn't solve are from my experience making this jam game, so if you need a reference to what I'm talking about, please just take a look at the game (it takes like 5 minutes).

Some prerequisite to make the system less broad:

  • Non-physics-based RPG-ish combat: meaning the resolution of an attack could be calculated before the attack occur (it doesn't need a physics system to determine that an axe has hit a skull).
  • Completely offline (not making a Homestone card game any time soon).

Anyway, going through the problems, order from "partially solved" to "absolutely no clue":

(It's a long wall of text, get some popcorn)

Situation 1: Unit 1 attack unit 2 with single target melee skill.

Expected behavior:

  • Attacker play an attack animation.
  • At the "hit" moment in the animation, defender plays a "got hit" animation (or dead, depending on remaining health), a floating notification popup showing damage number, defender's health bar plays a deplete animation.

My "solution":

[Game, Event(EventTarget.Self)]
public class AttackingComponent: IComponent {
  public Guid target;
  public float hitTime;
  public string skillType;
}
  • When the attacker start attacking, I call
attacker.AddAttacking(
  defender.id.value,
  gameContext.time.timeSinceLoad + skillConfig.animationDelayTime,
  skillType
);
  • UnitView: IAttackingListener react to new Attacking component and play corresponding animation.
  • A ResolveAttackSystem loop through all GameMatcher.Attacking and resolve the damage:
if (entity.attacking.hitTime<= gameContext.time.timeSinceLoad) {
  var defender = ...
  defender.ReplaceHP(calculatedDamage);
  gameContext.CreateEntity().AddDamageNotification(calculatedDamage, defender.position.value);
}
  • UnitHealthBarView: IHPListener subscribe to HP changes and play the tween.
  • MarkDyingUnitSystem marks unit.isDying = true; if needed.
  • UnitView: IHPListener subscribe to HP change and play damaged (or healed, or dead) animation, depending on before/after state.
  • ShowDamageNotificationSystem add a view for the damage text.
  • The unit.isDying -> unit.isDead thing is just a repeat of the Attacking-> ResolveDamage process so I won't delve into that.

What I don't like about this:

  • Notice the skillConfig.animationDelayTime? it's from a SkillConfig: ScriptableObject for every skill. What I don't like is the for every animation change, the designer/animator have to both change the animation, and (calculate then) type in the hit moment delay of the animation into the SO. Even as the creator, I forget about it all the time, causing hard-to-notice desync of animation & resolution.
    • Another option I discarded is to have an public void OnAttackHit() { } callback in the UnitView, and make use of Unity's AnimationEvent to call it at the correct frame. This make the game logic dependent on the view (not to mention the fabled Unity's bug with AnimationEvent).
  • Another choice was about determining which animation the defender should play. In the end I chose not to use DamagedComponent + HealedComponent and determine the animation based on how HP has changed instead. This result in the View having to cache unit's current HP (so it has something to compare new HP value with). Not sure if keeping game value in View is a good choice.

Any better option?

Situation 2: Unit 1 attack unit 2 with single target projectile-based skill.

Expected behavior:

  • Same as the melee use case, but at the "hit" moment, a projectile (arrow) is shot instead.
  • The arrow flies sexily toward the defender, and only when the arrow hit will the damage resolution process trigger.

This is actually not (that) hard. My solution was to consider every skill in the game projectile-based. The previous ResolveAttackSystem now become ShootProjectileSystem, which create a projectile instead:

public class Projectile: IComponent {
  public string type;
}
public class ProjectileFlyingBehaviorComponent: IComponent {
  public string type;
}
public class ProjectileSpeedComponent: IComponent {
  public float value;
}

Melee projectiles has a movementSpeed of 0 and flies to target instantly, while ranged projectile have an arbitary movementSpeed and a distinct fly behavior (straight or curved etc). Upon landing (sqrDistance < sqrDeltaSpeed) I now create an AttackResult, which is used by ResolveAttackSystem:

[Game, Cleanup(CleanupMode.DestroyEntity)]
public class AttackResultComponent: IComponent {
  public Guid attacker;
  public Guid defender;
  public float damage;
}

1 small note here is that because it's an RPG, the defender doesn't move, so for melee attack to work I only need to set meleeProjectile.ReplacePosition(defender.position.value); and it will trigger immediately. I am not sure if this will still work in a dynamic game, where the defender may run around (possibly even teleport).

Situation 3: unit attack many units with AoE damage skill.

You may think it's just a simple conversion to 1-to-n relationship, but it's not that simple. You see, in this screenshot:

screenshot 2018-10-07 20 59 19

If I make all floating numbers to popup at the same time, it would look bad. So instead I want a delay between numbers showing up, somekind of smooth wave-like cascading flating text instead of 9 numbers flying up in parallel.

Expected behavior:

  • Instead of having all hits resolved at the same time, have some kind of delay between them.

In the actual game, what I did was to throtte ShowDamageNotificationSystem, so it only create 1 new floating text each 3 frame.

This is a very bad idea because tween choice is limited. The ShowDamageNotificationSystem system doesn't know which notification that is, you can't have each skill do its own distinct floating animation (For example an Explosion!!! skill should have text floating in a circle from the center, while a Charge Mofo!!! skill should have text floating up in a wave from the front toward the back):

  • You are pretty much limited to a Linear time function (No "ease" of showing text).
  • The order of the floating text showing up is undetermined, because IGroup doesn't guarantee order.

I have a feel it's also wrong in a more fundamental way (like it's violating some ECS principles here), but can't put it into word. Maybe someone could tell me if it is.

So, in a later game, I made some changes. I split AttackingComponent into 2:

public SkillInProgressComponent: IComponent {
  public Guid attacker;
  public Guid defender;
  public string skillType;
  public float hitTime;
}

[Game, Event(EventTarget.Self), Cleanup(CleanupMode.RemoveComponent)]
public DoingSkillComponent: IComponent {
  public string skillType;
}

SkillInProgressComponent is used by the game systems to resolve damage, while DoingSkillComponent is subscribed by the view to display corresponding animation. The hitTime of each individual hit can be determined by whichever system creating them, allowing me to do whatever crazy idea I could think of.

What I don't like about it:

  • Don't really know where to put the hitTime calculation code in. A SkillService? Right now because there's only 2 different kinds of text animation I'm putting it all in the PerformAttackSystem within an if/else block. But obviously this is not scalable.
  • Then YESTERDAY, the designer came up with a brand-new, totally-not-copied-from-the-Disgaea-game-hit-was-gifted-last-week idea of having multi-hits skills. So now:
    • The skills will possibly have several hit moments, each with a damage coefficiency value (you know, a Thousand Punches!! skill where the initial 999 hits deal 1000 damage and a 1000th "Finisher" hit that deal 3000 instead?).
    • The defender will stay at the last frame of the "hurt" animation throughout the combo, and only revert to the idle animation after the skill has completely finished.

The first part is doable with current implementation, I only need to change SkillInProgressComponent it to have public float[] hitTime; together with a public float[] hitCoefficiency;

But still, "something" is ringing here. I don't know why but I have a feeling I'm going down a dark path. Maybe I should not change this to float[], or maybe the component becamse bloated? Again, can't put it into word.

For the second part (keeping defender at last hurt frame), still absolutely no clue. Any idea?

Situation 4: very long chain of back and forth skills that cause turn time to become varied.

Very long chain of back-to-back skill triggers, for example, a Vine skill deal damage, then cause the Poison debuff (visual the debuff after the attack). The defender happen to have a Counterattack so he perform a revenge attack. Attacker's ally have a Protect skill which make him block the attack for the attackers. He also has Counterattack so he does that as well.

Anyway, with the types of skill increasing, the total resolution time of the whole combo become undeterminable at compile time. In the example game, after player choose a skill, I would put a 2 seconds delay before moving on to the next unit's turn, and it worked great, because no skill combination could ever reach 2 seconds (it was a game jam, don't judge)

Expected behavior:

  • Slight delay between skill chaining (most importantly there should be a delay between Counterattacks)
  • Unit turn will end only after all skill chain has ended.

Now this is inside the "totally no clue" zone for me, but I'll try to input some idea: For the 2nd point, I'm thinking about have an NextTurnSystem: IExecuteSystem that count the number of entities that has either

  • GameMatcher.Acting (this component mark unit whose turn's up and didn't receive player input) or
  • GameMatcher.SkillInProgress (this component mark units performing some kind of skill).

If the both groups are empty for some time (let's say 1 second), the turn will end, when I mark nextUnit.isActing = true;. I have this idea from the way Mount&Blade determine how a battle has ended.

Still, puting a time counter in a system is... idk, again, "danger" bell ringing. I will most likely have to put "exception" cases in the system later, to account for game paused, battle ended etc.

For the 1st pointer, I can't think of any "clean" way to do. And it's a must because without a delay, the counterattack will happen immediately after the initial attack end, and it looks terrible.

So that was it. A lot of problems here and there, looking for input to improve/change. How do you do yours?

AVAVT avatar Oct 07 '18 20:10 AVAVT

I read Situation 3 Possible solution is Parametric Animation. It's doable with tweens, behaviour trees, or coroutines. During them create Input/Command entites, this will most likely happen during unity's Update stage, so be careful what entities you create.

Don't really know where to put the hitTime calculation code in. A SkillService? Right now because there's only 2 different kinds of text animation I'm putting it all in the PerformAttackSystem within an if/else block. But obviously this is not scalable.

I write functions into systems at first, then once needed elsewhere, refactor them into service and call from various places.

c0ffeeartc avatar Oct 08 '18 08:10 c0ffeeartc

I read Situation 3 Possible solution is Parametric Animation. It's doable with tweens, behaviour trees, or coroutines. During them create Input/Command entites, this will most likely happen during unity's Update stage, so be careful what entities you create.

Which part were you talking about? The delay between AoE hits, or the multi-hits part? (sorry I didn't organize the point very well).

Anyway, this part is an animation visual, so creating InputEntity wouldn't be a sound idea, would it?

AVAVT avatar Oct 08 '18 10:10 AVAVT

Which part were you talking about? The delay between AoE hits, or the multi-hits part? (sorry I didn't organize the point very well).

It should work for both parts, it's written in code and quite flexible. The downside it needs recompiling

Anyway, this part is an animation visual, so creating InputEntity wouldn't be a sound idea, would it?

Fine for me. Would like to know more solutions

c0ffeeartc avatar Oct 08 '18 14:10 c0ffeeartc

I haven't read all of your explanation yet, but for situation 1 (and in other async command) I will use an InputEntity with component that have a callback as property like this

[Input]
public class CmdPlayAnimationComponent : IComponent {
	public GameEntity Entity;
	public string Animation;
	public Action OnAnimationEnd;
}

and the reactive system that catch on this will just have to call the callback when it finished

public class AnimationSystem : ReactiveSystem {
	public void Execute() {
		//PlayAnimation is just function that will play animation and call callback when end
		PlayAnimation(cmd.Animation, cmd.OnAnimationEnd);
        }

and use a command like this

var cmd = CreateInputEntity();
cmd.AddCmdPlayAnimation(entity, "hurt", () => createCommandThatShowDamage());

and I combine the concept above with RSG Promise and it become like this

[Input]
public class CmdPlayAnimationComponent : IComponent {
	public GameEntity Entity;
	public string Animation;
	public Action OnAnimationEnd;
}


public static class CmdPlayAnimationExtension {

	public static IPromise PlayAnimation(this GameEntity entity, string animation) {
		return new Promise((resolve, reject) => {
			Contexts.sharedInstance.input.CreateEntity(e => e.AddCmdPlayAnimation(entity, animation, resolve));
		});
	}
}

and use it

entity.PlayAnimation("hurt")
	.Then(() => createCommandThatShowDamage());

hope this help somehow

Arpple avatar Nov 07 '18 10:11 Arpple