Open AVAVT opened 6 years ago
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.
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?
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
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
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:
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:
My "solution":
UnitView: IAttackingListener
react to new Attacking component and play corresponding animation.ResolveAttackSystem
loop through allGameMatcher.Attacking
and resolve the damage:UnitHealthBarView: IHPListener
subscribe to HP changes and play the tween.MarkDyingUnitSystem
marksunit.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.unit.isDying
->unit.isDead
thing is just a repeat of theAttacking
->ResolveDamage
process so I won't delve into that.What I don't like about this:
skillConfig.animationDelayTime
? it's from aSkillConfig: 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.public void OnAttackHit() { }
callback in theUnitView
, 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).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:
This is actually not (that) hard. My solution was to consider every skill in the game projectile-based. The previous
ResolveAttackSystem
now becomeShootProjectileSystem
, which create a projectile instead: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 anAttackResult
, which is used byResolveAttackSystem
: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:
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:
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 anExplosion!!!
skill should have text floating in a circle from the center, while aCharge Mofo!!!
skill should have text floating up in a wave from the front toward the back):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:SkillInProgressComponent
is used by the game systems to resolve damage, whileDoingSkillComponent
is subscribed by the view to display corresponding animation. ThehitTime
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:
hitTime
calculation code in. ASkillService
? Right now because there's only 2 different kinds of text animation I'm putting it all in thePerformAttackSystem
within anif/else
block. But obviously this is not scalable.Thousand Punches!!
skill where the initial 999 hits deal 1000 damage and a 1000th "Finisher" hit that deal 3000 instead?).The first part is doable with current implementation, I only need to change
SkillInProgressComponent
it to havepublic float[] hitTime;
together with apublic 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 thePoison
debuff (visual the debuff after the attack). The defender happen to have aCounterattack
so he perform a revenge attack. Attacker's ally have aProtect
skill which make him block the attack for the attackers. He also hasCounterattack
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:
Counterattacks
)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 eitherGameMatcher.Acting
(this component mark unit whose turn's up and didn't receive player input) orGameMatcher.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?