NodeCanvas Forums › Support › Modifying NodeCanvas?
Hi,
I’m modifying NodeCanvas slightly to suit our needs more closely. I’d like to discuss this change with more experienced users on the best way to implement a change, or whether it is necessary in the first place.
We’re interested in using NodeCanvas to help with chatacter state management – specifically, given the state of the character, what actions are available?
Previously this was done in a somewhat ugly way. Each ability would have it’s own “state machine”, describing the state of the ability. They were simple, operated through System.Enum
s and sometimes only had two states – Active
and Idle
. Some had sub-states; for example, MeleeState.Attack
had substates MeleeAttackType.Chop
and MeleeAttackType.Stab
, then those had different phases: MeleeAttackPhase.Anticipation
, MeleeAttackPhase.Action
and MeleeAttackPhase.Reaction
.
We wanted to ensure that only one ability could be triggered at a time – that is, in order for an action to trigger, it’d have to check the states of every other action being performed to make sure they were all idle. Because of this it was difficult to know what actions were possible at any given time due to how spread out this process was. It also coupled a lot of stuff together.
Using a state machine would simplify this problem and make it much more intuitive to work with.
That being said, many of our actions operate in this fashion: Character abilities would be represented as one or more behaviours. They’d have access to the current state
of their associated state machine, as well as the current progress
through that state. Based on those parameters, they’d execute different code. Usually the current progress
value would drive the progress through an animation clip of the given action.
This seems to run contrary to the intended usage of NC’s state machines. Rather than Behaviours reading into the state of the state machine and executing differently, the state machine itself runs the behaviours and the behaviours themselves have no awareness of their place in the state machine.
While I agree that the latter approach is probably better in at least the majority of cases, we cannot really afford to rewrite all of our behaviour code. A more feasible alternative seems to me that we should have our behaviours read the state of the state machine as before, but where our ugly pseudo-state-machines are replaced by a single NC state machine.
So far to accomplish this goal, we’ve changed Node.cs
so that in addition to a name, tag and description, nodes may also have a System.Enum
. This has advantages over strings in that it’s difficult to verify whether you’ve made a typo, whereas if you’ve done so with an Enum, the compiler would tell you right away. This is working great so far.
This is where I’m currently stuck:
The MeleeState.Attack
state contains a nested FSM with five states. The first state is a dummy “start” node that immediately transitions into one of the other four states depending on conditions attached. The other four states are the four different attack variations that each melee weapon has: MeleeAttackType.SwingLeft
, MeleeAttackType.SwingRight
, MeleeAttackType.Chop
, MeleeAttackType.Stab
.
Those four states too are nested FSM’s with three states: MeleeAttackPhase.Anticipation
, MeleeAttackPhase.Action
and MeleeAttackPhase.Reaction
.
We need to be able to control the timing of state changes depending on whether you are in the MeleeAttackPhase.Anticipation
, MeleeAttackPhase.Action
or the MeleeAttackPhase.Reaction
phase of the attack. In other words, the duration of each one of these states differ. Ideally, we’d like to have a single ActionTask
on each of these states, and then use the Enum
value associated with the FSMState
as lookup parameters for the weapon’s timing stats. However, it appears that it is currently impossible to access the parent Node
from within an ActionTask
.
Furthermore, we need to be able to control the timing of state changes depending on what type of attack you are doing. That is, the rate at which MeleeAttackPhase.Anticipation
progresses into MeleeAttackPhase.Action
differs depending on whether the “parent” state is MeleeAttackType.Chop
or MeleeAttackType.Stab
. It appears that there is currently no way to determine the Enum
value of “parent” states, even if it were possible to access the Node
from within the ActionTask
.
I am considering changing the code so that it is possible to navigate the node heirarchy from any point in the FSM similar to how GameObjects operate, but this seems like a big undertaking. I thought I’d ask for advice before I start.
Thanks in advance for any help you may provide! 🙂
Hello!
Thanks for the detailed info and sorry for the late reply.
If I understood the issue correctly down to it’s core, the problem is that you want to control the timing for a transition to happen, rather than taking place immediately. So basicaly something like a time based condition.
Here is a Timeout ConditionTask. Basicaly, what it does, is that it only returns true after a period of time has passed while it is checked.
You could use this Timeout condition along with some other conditions within a ConditionList on the transition, which is set to “ALL TRUE”.
As such, the transition will take place only when all the conditions are true, including that the Timeout time has also come to pass.
Then as far as setting the timer, it can be done so through it’s parent state by setting a Blackboard Float Variable, and setting the Timeout condition ‘timeout’ BBParameter to read from that float variable.
Please let me know if that works for you, or maybe I completely misunderstood the actual issue 🙂
Thanks!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using NodeCanvas.Framework; using ParadoxNotion.Design; namespace NodeCanvas.Tasks.Conditions{ [Category("✫ Utility")] [Description("Will return true after a specific amount of time has passed and false while still counting down")] public class Timeout : ConditionTask { public BBParameter<float> timeout = 1f; private float currentTime; protected override string info{ get {return string.Format("Timeout {0}/{1}", currentTime.ToString("0.00"), timeout.ToString());} } protected override bool OnCheck(){ if (currentTime >= timeout.value){ currentTime = 0; return true; } currentTime += UnityEngine.Time.deltaTime; return false; } } } |
Join us on Discord: https://discord.gg/97q2Rjh