Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

NodeCanvas - (Behaviour Trees | State Machines | Dialogue Trees)

Discussion in 'Assets and Asset Store' started by nuverian, Feb 8, 2014.

  1. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello,
    Sorry for late reply. I haven't test this on windows mobile, but I see no reason why it shouldn't work on the platform.

    Cheers
     
  2. sonicviz

    sonicviz

    Joined:
    May 19, 2009
    Posts:
    1,051
    Hi,
    Thanks for latest update with namespace.

    Last demo Iterator gives an error on the Action node "Call Translate '0' (by reflection?) when you click on it
    :There are no options to select for method name" and then the Action changes to No Method Selected on the node itself and nothing changes.

    Bug?
     
  3. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello. You are welcome.
    Please try reselecting the type first and that should fix it. I realized this after pushing the update.
     
  4. ClaudioFreda

    ClaudioFreda

    Joined:
    Nov 16, 2013
    Posts:
    7
    Thanks, that was a very good answer and very thorough. I'm liking your attention to detail and it seems that you actually promote proper programming practices, unlike other visual tools i've worked with.

    You should highlight these features more, as they're a strong point it seems. They're one of the first things I look in a visual tool and I'm sure I'm not the only one.

    I have some follow-up questions:

    What would be the advantage of using NodeCanvas Events rather than unity messages?

    Does NodeCanvas support sending custom types (mostly structs) through events/unitymessages/methodcalls?

    What about using a MonoBehaviour I already implemented as a sort of custom action?
     
    Last edited: Mar 31, 2014
  5. sonicviz

    sonicviz

    Joined:
    May 19, 2009
    Posts:
    1,051
    I don't know what you mean?
    You select it and there is only Component and Method Parameter Type, neither of which do anything if you select them.
    Set to Transfer/Behavior and int32
     
  6. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello and sorry for late reply,

    Thanks. I try to. I'm very bad at promoting my work if you know what I mean :)

    Q. What would be the advantage of using NodeCanvas Events rather than unity messages?
    A. The NC events are there to simply help intercommunication between nodes of the graph like the examples I've posted on the initial answer. They are nothing more advanced than that.

    Q. Does NodeCanvas support sending custom types (mostly structs) through events/unitymessages/methodcalls?
    A. Im a bit confused with this question cause I don't understand send where? :) If you mean you want to send a message to the agent by all means you can make an action and send a unity message with your type as you normaly would. If you refer to the included action to execute a method, that's an action to help minimize the amount of custom actions you need to make. This specific action supports one parameter of any type supported by the blackboard since the value is taken from it most of the times.
    Are you refering to something else?

    Q. What about using a MonoBehaviour I already implemented as a sort of custom action?
    A. You will have to make a custom action deriving from ActionTask, but this is actually a good idea. I might look into it as of how to minimize the amount of work need or even make it automatic.
    A workaround would be to create a custom action that takes a MonoBehaviour as a reference and control that monobehaviour from that custom action.

    I hope that is helpfull? If you have any more question please ask :)



    Hey,
    In the Execute Method action, you first select the Component you want to call a method on, then the Parameter Type of the method you want to call (or property to set) and finaly the method (or the property setter). If there are no available methods on the selected component with the selected parameter type, then you will not see a dropdown of methods, but instead you will get a "No option to select." warning.
    That is why you see no options for the selection of Transform + integer. Select Vector3 for Parameter Type and you will see dropdown of methods and property setters.

    Let me know if that works for you :)
     
    Last edited: Apr 1, 2014
  7. sonicviz

    sonicviz

    Joined:
    May 19, 2009
    Posts:
    1,051
    ah, I see! ty!
     
  8. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    I have grave concern about Node Canvas after using it for a while.

    If you are using it only on couple of so many game objects it is fine, but when you have lots of them.. I mean lots, like 100+ of them then I think current approach is not good for performance wise.

    This is primary because everything Node Canvas doing is based on creating a "Graph node game object" parented under the game object with BT or FSM.
    If you have many nodes , each node have game object parented along another game object for connection. This is huge number of game objects nested in the parent object. And you multiply it by X game objects using BT.

    So for instance, if you have 200 enemies using Node Canvas AI nodes, then this becomes 200 X Node number X 2 + etc

    Now this is not an acceptable increase of game objects in the scene. If you look at the current approach of mechanim it precisely optimises game objects by giving you an option to hide joints etc..

    If Node Canvas wants to be really good AI solution, then this need to be fixed ASAP. You do not want Unity engine to calculate transforms of every nodes and connections. Take look at Playmaker and it does this very well.

    You will almost never going to go through graph nodes in the scene manually so there is no point exposing them this way and increase overhead on performance. This is especially important for mobile devices.

    What do you say.
     
    Last edited: Apr 2, 2014
  9. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    How do I conditionally only move to the next state in FSM from nested BT upon BT returning failure? (Bt running a single sequencer returning failure on some condition)

    I have tried inverted True condition, but it doesn't seem to work..

    Edit : realized True condition was not about detecting if BT returning failure or success. Is there way for FMS states to know if nested BT has returned failure or success? (or rather does BT returns failure or success at all?) If I can't find out about what happened about result of nested BT in my FSM then it's pretty much bit useless for me..
     
    Last edited: Apr 2, 2014
  10. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    Anything? This is blocking issue for me!

    I had to work it around by adding bool variable and extra setup inside BT... which I think BT should return the value by default anyway...?
     
    Last edited: Apr 2, 2014
  11. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hey there.
    Sorry for late reply.

    200+ agents is too much regardless. Have you tried having that number with playmaker? :p
    Be asured that in my part I continouelsy do optimizations but it is also up to the design of the behaviour up to a degree.
    More optimizations will sure take place.


    I've added that "feature" now, where in the Nested BT node you select an event to be fired when the BT assigned finish in Success and another event when it finish in Failure (root state). Then you just check the transitions with the Check Event condition.
    Of course that take place if you have set the BT to run once rather than forever.

    Instead of you having to wait for the update. PM me your email and I will send you the new version right away.
    There are more things included in that version that I will post about later.

    Thanks and sorry for late reply.
     
    Last edited: Apr 2, 2014
  12. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    Well, I don't think 200 enemies are too many.. it all depends on the game really.. for instance, if you build a tower defense game or some arcade shoot up then 200 simple AI controlled enemies are pretty common. Also, I was specifically asking about the number of extra game objects Node Canvas creates and extra game objects per node sounds like bit too many for anyone anyway. This is why I have mentioned about Mechanim as Unity guys knows that many game objects in the scene will hit the performance hence they introduced hiding the joints from the scene. If you look at their demo, they have many many characters running around the scene. Telling us 200 is too many is bit absurd.

    I am perfectly aware that using node based AI like Node Canvas or Playmaker etc has additional overheads, than doing it purely on code base. I can accept that but I think one must try and minimize over head where ever possible and reducing that many number of game objects Node Canvas generate is important one. Why do you want us to have this feeling of " oh I am adding another game object (s) " whenever I click to add any nodes in Node Canvas? That is just a common sense I think..

    Anyway, I don't know, but it could be bit too late to make that kind of design change to NC... so I am just throwing down here... hehe

    Also , I found that conditional check for float , int is not good enough ( well not complete enough ). This happens when you want to check for "less than". Because you can't just invert condition on "greater than" , obvious reason being that it will miss "equal to" as well. So invert of > is not the same as < it is rather the same as <=

    So in order to make this more complete conditional check (at least for int , float types) it needs to handle more cases. You could argue that I can just "add" more nodes and do additional checks one after another, but that is just not a clean and better way I think. You would want to make your nodes as few as possible whenever possible. (especially if it is adding yet another game object in my scene)

    I have been modifying the conditional scripts to make this happen as I go but just to let you know about it so it can possibly updated with improved version next time.

    Also, with new BT node event firing feature, can I run my BT to forever and still check for that event to transit when it happens? I want to just wait and keep checking if it becomes either success or fail (not both)

    Pmed you.
     
    Last edited: Apr 2, 2014
  13. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Each system is build differently. There are pros and cons in each design aproach, but using GOs that generaly do nothing, might impact performance marginal if at all. Mecanim does what it does because they actually do something.

    I've added a less than, sure why not.

    You can minimize the amount of actions or conditions needed by using Action List and Condition List Tasks under 'Systems' category, so that it's more clean. It's a matter of preference on how you'd like to design the BT.

    You can use those events fired by the Nested BT node with forever running BTs and check for either Succes or Failure root state.

    I've send you the email :)
     
  14. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    I mean, surely that doesn't seems optimal? Simple logic tells me that if I can do "greater than" with single condition check then I should be able to do other way around using single check too. It's a simple improvement really.... And rather than having another script to do less than check, why not make choice available in one? I have no idea why would anyone wants to do it any other way? Even if it is possible to do it by using conditional list, why would you want to??? why would you check for inverted == and then inverted > for simple < check? Come on now, this isn't even needed discussing....;)

    Here is my Conditional Int check that I have made if anyone is looking for one :

    I could have used switch statement for OnCheck but I am not sure which will be faster..

     
    Last edited: Apr 3, 2014
  15. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    Is there way to set order of action or condition choice list? I have feeling that the list will grow as I develop my app and it would be nice if there is at least some sort of ordering system or sorting system so I can make the list more readable and manageable and be able to find them quicker?

    Is there way to copy paste nodes? or at least relink nodes without losing conditions? Having to remake the link conditions or node's actions is real pain..

    BTW, nested BT event sending works well and thanks for that.
     
    Last edited: Apr 3, 2014
  16. Atanvaryar

    Atanvaryar

    Joined:
    Sep 1, 2012
    Posts:
    9
    Hi there! I just bought NodeCanvas and trying different stuff out and I got a quick question.
    How would I go about writing an action for a NodeCanvas FSM to display some OnGUI stuff?

    Thanks!
     
  17. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    You are welcome.
    You can reorder fsm actions in the list by using the arrows. Do you mean something else?
    I am working on relinking and dublicating :)

    Hello and thanks :)

    Right now there is no special handling of GUI stuff so things might change, but you can simply do this:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using NodeCanvas;
    4. using NodeCanvas.Variables;
    5.  
    6. [ScriptCategory("GUI")]
    7. public class DisplayGUILabel : ActionTask {
    8.  
    9.     public BBString text;
    10.  
    11.     void OnGUI(){
    12.  
    13.         if (!isRunning)
    14.             return;
    15.  
    16.         GUILayout.Label(text.value);
    17.     }
    18. }
    19.  
    Check isRunning so that GUI displays only when it should. Also make sure to call EndAction if and when you want so. This example action never ends.

    Here is another one, which shows a GUI button and sends an event when clicked. You can use the CheckEvent Condition on the outgoing transitions to catch it.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using NodeCanvas;
    4. using NodeCanvas.Variables;
    5.  
    6. [ScriptCategory("GUI")]
    7. public class DisplayGUIButton : ActionTask {
    8.  
    9.     public BBString buttonText;
    10.     public BBString clickEvent;
    11.  
    12.     void OnGUI(){
    13.  
    14.         if (!isRunning)
    15.             return;
    16.  
    17.         if (GUILayout.Button(buttonText.value)){
    18.             SendEvent(clickEvent.value);
    19.             EndAction();
    20.         }
    21.     }
    22. }
    23.  
    This might slightly change though to remove the need of checking isRunning as well as optimization.

    (PS: In these examples I am using the BB variables to make possible to chose a value from the blackboard, but of course that's not mandatory)

    Thanks :)
     
    Last edited: Apr 3, 2014
  18. Atanvaryar

    Atanvaryar

    Joined:
    Sep 1, 2012
    Posts:
    9
    Ni Nuverian, thanks a bunch for the quick reply.
    I tried it and it worked perfectly, although I get a weird error in the log.

    I have two states in the FSM, one is empty, and one is calling the DrawPerformanceCounters action I wrote. The transition between them is made by a keypress. Now at startup everything is fine, the gui doesnt draw, and the editor shows empty state running waiting for input. When I press the designated button it correctly switches state and now shows the GUI.
    But when I press the button again to switch back it switches correctly to the empty listener state, but logs an error, saying that the state has no actions on it and asks to add an action.

    On another note, I also found a tiny bug in the code. When trying to build the game for standalone it gives a parse error in the EditorUtils.cs
    I've tracked it down to this:

    EditorUtils.cs
    Line 664 #endif

    should actually go to

    Line 667

    otherwise the #if UNITY_EDITOR at the start breaks the namespace curly braces.
     
  19. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello,
    You are welcome

    Yes the #endif placement bug has been found couple of posts before and has been fixed. I missed that when adding namespaces :)
    If you refering to the warning log "State has no actions", it is harmless but has been removed as well :)

    Both and a lot more, are in the new version that I will submit today

    Thanks! Let me know if you have any more questions
     
  20. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Will there be some sort of a video tutorial series to get show how to get simple actions and NPC like behavior going or any kind of quick start video? I have read all the documentation and this seems to be perfect for our game and it has come at the right time, but usually implementation and getting assets to work with your project can be easier seen with a more in depth preview. Your solution is very high on the list.
     
  21. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello there,

    I was supposed to deliver some videos last weekend, but sadly I didnt because I wanted to get the new version up soon and record the video with that new version. With that now done, I hope to deliver some videos this weekend.

    Thank you for considering NodeCanvas and let me know if you have any questions :)
     
  22. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    So here is what's new in version 1.3 (pending):

    • Optional Icon mode. (BTs for now) that generaly take the same space as the text mode, so layout will stay the same.
    • State Machine transitions will now be automatically layouted. As such changing input output sides is removed and obsolete.
    • Removed nodes from the top of the editor. They are now added with right click on the canvas (frees up even more space to work on).
    • Nested BT in State Machines will optionaly fire an Event when the BT's root returns either Success or Failure. (To be used along with Check Event condition on the transitions).
    • Nested FSM in Behaviour Tree will now return Success or Failure based on the current FSM's state.
    • Dublicate Nodes with Control + D is now possible.
    • Dragable Action Conditions Task titlebars.
    • Right click context menu on Task titlebars to open script editor.
    • Replaced "=" with a radio button (when choosing blackboard variables).
    • Fixed some Editor UI bugs.
    • Added a Condition to check a CSharp Event (Select component, select event to subscribe. When it is raised, condition becomes true).
    • Added even more examples, some Actions and Conditions.

      --EDIT--
    • Inherited public Task fields will also show in window inspector
    • Public Task properties with both get set will also show in window inspector


    $IconMode.png


    $FSMLayout.png
     
    Last edited: Apr 7, 2014
  23. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    By ordering action, i mean not the action list i have selected ones... i mean the... context menu.. when you click to add action the lisr pops up category and then the list of actions in that category. I was wondering if there is way to order that context list of actions pop up. Right now it seems lie first one found first displayed or something.. so it would be great if we have option to eith sort them in some more logical order.. or even better if we can specifiy order it would be super. At the moment only thing i can do is put them in the category.
     
  24. HDD

    HDD

    Joined:
    Apr 7, 2014
    Posts:
    3
    Thanks for creating such a cool plugin Nuverian. Unfortunately I'm having some troubles with it at the moment and I was hoping for some help.

    I'm attempting to Instantiate characters that are using the same BT and Blackboards. The BT and blackboard are setup in a similar way to your "Soldier" example scene and the BT works exceptionally well if the character exists within the scene before Play, but if I instantiate the character it's Blackboard variables are all there but are empty, which then causes the BT to return an error when it checks a node.

    Other than this issue, everything has been running well and I'm quite impressed with the amount of time and effort your putting in to keep this updated. Keep up the good work and thank you in advance
     
  25. Vectrex

    Vectrex

    Joined:
    Oct 31, 2009
    Posts:
    267
    There's a current problem with prefabs and adding blackboard variables to the scene objects.
    I think it's a 'SetDirty' problem after the change as I had a similar issue with something I made. Easy to fix though.
     
  26. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Thanks a lot for your comments! :)
    Sorry for being so late, but I was/am on a trip.

    @HDD @Vectrex

    If you are refering to having a scene object reference to be 'saved' in a blackboard prefab asset, it is a common unity thing, that a prefab asset can not have references to scene objects and references are lost / not serialized, unless that object is part of the prefab as well. That is not an NC thing :)

    Now I had some thoughts on how to work around and allow this, since the blackboard can already save load itself.
    Here is a very temporary solution:

    1. Open Blackboard.cs and add the ContextMenu attribute over the Save method. [ContextMenu("Save")] (line 227)
    2. Make your blackboard variable assignments before play as usual and 'save' the blackboard through the gear icon of the component by that new Save command.
    3. In play mode and after instantiating the gameobject with the blackboard, call Load() on that blackboard.
    (keep in mind that the blackboard name is important)

    I will make this automaticaly in the future.
    Let me know if this works as a solution for you.

    Thanks
     
    Last edited: Apr 7, 2014
  27. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Oh I see what you mean now :)
    I can't think of any other ordering other than alphabetical (which right now is not silly me). Do you have anything other in mind?
    By the way, you can also specify subcategories, by using "\" in the category names like for example : "Interop\Variables"
     
  28. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    I'm glad to say that version 1.3 is now live.
    You can find the changes couple of posts above or in the first post :)
     
  29. Marble

    Marble

    Joined:
    Aug 29, 2005
    Posts:
    1,268
    Cool.

    Is it necessary for the script name for a custom task to match the class name? I was hoping to bundle together a bunch of AI conditions into a single script, but when there is a mismatch the classes (despite being in the right namespace) don't show up in the menu for adding tasks to the graph.

    EDIT: I just encountered a use case for a weighted random selector, so let me toss in another request for that.
     
    Last edited: Apr 10, 2014
  30. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello there,

    Sorry, but you can't have many Tasks per script file because they are derived from MonoBehaviour and as such Unity doesn't allow this. :)

    I could add a probability selector where it would be possible to distribute a..normalized probability among child nodes and pick execution based on those. Is that what you mean?

    By the way, another not distributed way you could go about this now, would be to create a Condition Task that returns Success or Failure based on a random factor and assign that condition on the child connections of the Selector. Something like this:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using NodeCanvas;
    4.  
    5. public class RandomSuccess : ConditionTask{
    6.  
    7.     [SliderField(0,1)]
    8.     public float probability = 0.5f;
    9.  
    10.     protected override string conditionInfo{
    11.         get {return (probability * 100) + " %";}
    12.     }
    13.  
    14.     protected override bool OnCheck(){
    15.         return Random.Range(0,1) <= probability;
    16.     }
    17. }
    18.  
    Of course this is not the same and requires some extra work assigning those conditions on the connections, but just a temporary alternative suggestion to simulate almost the same effect. I will see to adding the requested selector.

    Thanks :)
     
    Last edited: Apr 10, 2014
  31. Marble

    Marble

    Joined:
    Aug 29, 2005
    Posts:
    1,268
    Yep, you got my gist. A selector based on normalized probability is what I was hoping for. Does your alternative guarantee that success will be returned, though?

    I imagine that something like this:

    Selector
    > Attack (0-30%)
    > Defend (30-80%)
    > Flee (80-100%)

    Is different from this

    Selector
    > Attack (30% chance)
    > Defend (50% chance)
    > Flee (20% chance)

    Because the latter has a chance that all three nodes will return failure, whereas the former will always return success from one node or another. I can see expanding on your example condition so that there is a minimum and maximum for the Random.Range, but then you can see how it becomes a pain to set up and adjust each condition.

    Anyway, thanks for listening!
     
  32. Marble

    Marble

    Joined:
    Aug 29, 2005
    Posts:
    1,268
    One more question: is GetComponent used to fetch the Agent on custom tasks and conditions? And is this called per node instance? I just wonder whether, if I have a complex behavior tree of e.g. 100 nodes and something like 20 npc characters using instances of this behavior tree, 2000 GetComponent calls at initialization might increase my scene loading times?
     
    Last edited: Apr 10, 2014
  33. castor76

    castor76

    Joined:
    Dec 5, 2011
    Posts:
    2,517
    Yeah I want to know this as well. Marble, I don't think this depends on if your node is custom or not. It seems all other default action and conditions are indeed just like another custom ones. (ie separate monobehaviour) I have mentioned earlier about the concern on this relating to requiring every node needs a game objects.

    If you have 20 characters with 100 nodes then you will have 20x100 = 2000+ extra game objects with each monobehaviour components on them. (more, depends on if you have more actions per nodes or if you have conditional check between nodes, which are yet additional game objects and monobehaviour components on top of them). This is my only deep concern about Node Canvas.

    Author says this shouldn't affect the performance much, but hey... they are still extras in the scene. Perhaps memory is going to be issue in the long run, or like you said getcomponent calls etc.. who knows? It is just uncomfortable feeling that every time I add something to NC, I will be adding these things in the scene...I have told author that if this game object moves (ie parent object with BT or FSM owner changes its transform) then Unity will have to calculate the transform of the child nodes (all these extra game objects) with it, which will have to have "some" over heads in the performance. I can't believe this would be free?? Perhaps this would require some more proper deep testing, but I believe every bit counts especially in the mobile platforms.

    It seems like it is too late now to change this as it is how NC is designed around. Just my honest opinion.
     
  34. Ecocide

    Ecocide

    Joined:
    Aug 4, 2011
    Posts:
    293
    I wonder if this asset could help me setting up a GUI menu. In my current project, I have a lot of different panels like Main, Options, Campaign, Credits etc. Right now I use Playmaker to build a FSM where each node resembles a panel.
    An example for my current solution: As soon as I change the state to Main, I disable all panels, that might have been the last node. In this case it would mean disabling Options, Campaign and Credits Panel (and I have even more because of a multi monitor setup). The reason is that with Playmaker I can't find out what the last node was and what panels were attached to it.

    It is kinda hard to explain. I just wonder if anyone every set up a GUI with NodeCanvas and could give me a hint.

    Thanks :)
     
  35. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    No, of course it doesn't guarantee success as it's not the same :) I just suggested an alternative that is 'kind of' simmilar to introduce a bit of randomness.

    The agent is passed on execution of the behaviour tree through the nodes and finaly down to the tasks, so it is known. A GetComponent will be called only once on initialize of the Task and only if the Task requires a specific agent type (by using the AgentType attribute), otherwise no, it's not called.

    --
    @Castor
    NC doesn't even use those GOs rather only the components. They just exist for organization and that's all. It could be done so that all components were added on a single game object, but that wouln't be organized at all and I think it's not really going to make that of a difference.
    If you indeed do some tests or comparisons, I will be glad to know.
     
  36. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello,

    NodeCanvas has a State Machine system that you could use for such a thing.
    It's a bit hard to understand exactly what you need though :) Is it basicaly to show a panel (ngui?) while disabling all others based on the current state?
    Do you need the last state as to disable specific panels and not all?
     
  37. ProjectOne

    ProjectOne

    Joined:
    Aug 9, 2010
    Posts:
    442
    I always have the same problem with all the Visual/Logic editors, etc...

    I find it hard to understand if I really need it because I find it hard to assess what benefits will bring to me.
    Suggestion:
    Make a video showing how you use NodeCanvas within the process of making a game, how does it save you time, how you can build reusable elements... how I can gain using it, immediate benefits and long term benefits.. Listing all the features will never cut it for those who do not already know exactly what this is :)

    I wrote a similar post on another similar product... In other words, show us do not tell us... You will possibly end up with more customers (expand from selling to those who already know and possibly already have their own versions of this.. to those who are less familiar with this and after seeing the benefits will add it to their tool set)

    Time you will spend on videos showing useful practical implementation/use of your asset will be time well spent and pay you back many times over. But as I mentioned on some other thread... do not do videos showing very generic abstract user case/examples... show your asset within the process of making a game, even the simplest of the games.
     
  38. iamsam

    iamsam

    Joined:
    Dec 22, 2013
    Posts:
    233
    Wow, couldn't have phrased it better (it is exactly what I was going to suggest). I had asked for videos a couple weeks back and still have not seen any examples by the author. I think the author should understand that this is the most competitive plugin in Unity with new editors coming up almost everyday (Behavior Designer (has movies), uFrame (a bit different concept but has movies), Behavior Machine (movies requested) to name a few recent ones). I am almost on the edge of buying one that suit my needs but it has become a difficult decision as I am not sure how to evaluate those without any examples/videos.

    Also, it would be great if you could explain the benefits of using FSM from your tool as compared to playmaker since it really puzzles me as to why would you promote compatibility with Playmaker when your tool itself includes FSM functionality. Thanks.
     
  39. Marble

    Marble

    Joined:
    Aug 29, 2005
    Posts:
    1,268
    To contribute a bit of light, I am currently creating a behavior tree for AI in code in parallel with NodeCanvas, to see which one is going to best suit the project. Some thoughts:

    1) I think I'll end up using the NodeCanvas tree in the end because its debug capabilities are excellent. I can watch, stop, start, and tick the system, with visual feedback coded into each task on the canvas, to see just what my AI is up to at any given point during runtime. This is awesome!

    2) I'm surprised at how sprawling my behavior tree has to be to match my code. I've never used visual scripting tools, but I am beginning to appreciate just how succinct written logic can be.

    3) Anxiety about performance, workflow, etc. is a stage I always have to tackle when using a powerful third party tool. I feel insecure having to rely on someone else for fixes and pipeline improvements, but I'm always encouraged by rapid feedback from the developer, like in this thread.

    4) This is part of the above, but I sometimes feel like I'm fighting the natural workflow of the tool by trying to do it my way. NodeCanvas is flexible enough that this is possible, which is great, but all my tasks require an attribute for a custom agent type, for example, which makes me think I'm walking blindly into a performance trap later on.

    5) I'm impressed by how simple NodeCanvas is. I don't have to familiarize myself with a sprawling API reference to become productive with it. The blackboard has two essential functions, the actions and conditions have a straightforward template; combined with the editor, I get a lot of power without a lot of hassle.
     
  40. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    @ProjectOne: Actually, well said. Show don't tell, I can't disagree :)
    @iamsam Sorry that I wasn't able to make those videos yet.

    I was delaying the videos because I wanted to record them on the new version 1.3 and then I suddently went off-site on a bussiness trip for a week, from which I actually just arrived back :)
    Now I know the need of videos and I am completely aware that it possibly holds people back. With my return from that trip back on-site I hope to make them asap.

    Regarding nodeCanvas's State Machines vs playMaker ones, the main difference is that NC's are condition based instead of event based. So if for example you want a transition to happen because a said target object is near enough, you make/use a condition that simply checks distance on that transition instead of making events and then having actions which fire those events. (although you can simulate that as well, by using the CheckEvent condition). So Action and Condition is different. Playmaker doesn't work with that concept (I just say that's it's different and not which one is better).

    Also, you can mix NC's State Machines with NC's Behaviour Trees in a coherent workflow, by having nested State Machine in a Behaviour Tree or nested Behaviour Tree in an State Machine, and the ways to communicate between one another.

    Last but not least the fact that you can reuse a condition or action as is, in both systems, I believe is a strong one :)

    Now the reason that there is a playMaker integration is a simple one. Many people like using playmaker and I can understand that, so they can still do so :) Another reason is that playmaker comes with tons of premade actions while NC not with so many so it's usefull for people that do not script at all. Something that I hope to change over time.

    @marble
    Thank you for providing an insight opinion for everyone :)

    I realy hope my next self-initiated post would be comming along with a video :)
     
  41. Marble

    Marble

    Joined:
    Aug 29, 2005
    Posts:
    1,268
    Is it part of general Behavior Tree design to have only a single input for every node? I have an Action list that gets used a lot. I could create a nestedBT for it, I suppose, but it would be more succinct to just funnel inputs to it from several different sources. (Or at least let me duplicate a node.)

    Also, it would save some frustration if the pop-up panel when viewing a node ate input events. I'll often click on a text box on the panel and find that NodeCanvas thinks I've clicked on a node obscured behind it.

    One more UI point: could nodes created with the right-click menu be created at the mouse pointer's location please? Right now, if I've scrolled down into a long behavior tree, new nodes appear somewhere above and I have to scroll to find them.
     
    Last edited: Apr 12, 2014
  42. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Hello Nuverian,

    I am the gentleman who contacted you through email with questions in regards to our game and using Nodecanvas in a networked environment. Great news! I have successfully gotten NC to work BEAUTIFULLY with our game's networked environment. I haven't gotten down to the advanced nitty gritty as I am still trying to learn and figure out NC fully. So far it seems like this solution will do 1000x better than our old complete code based behaviors.

    So down to business. How I am currently testing the solution is like this. Server is fully authoritative, AI spawner spawns an AI master unit on the server, this does all the logic. Proxy units are spawned on the clients, these have scripts (so far, still figuring things out with NC) that receive RPCs and state syncs to keep them synced across the network. So here is the problem I am running into. I want to populate a list of GameObjects that will hold all the players connected to the server. The AI will be able to use this list to decide who is close and who to chase after. Simple. I have this working fine using my own list in code, but my question is, how do I add and remove from the Blackboard GameObject list? I want to ideally use a blackboard for all AI that have access to this player list. This is how I have done it now:

    Code (csharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using NodeCanvas;
    5. using NodeCanvas.Variables;
    6.  
    7. [ScriptCategory("Blackboard")]
    8. public class AIBlackboardVariables : ActionTask {
    9.  
    10.     public Blackboard aiBlackboard;
    11.         //Should I be using BB variables here?
    12.  
    13.     public float AIHealth
    14.     {
    15.         get { return aiBlackboard.GetDataValue<float>("AIHealth"); }
    16.         set { aiBlackboard.SetDataValue("AIHealth", value); }
    17.     }
    18.  
    19.     public GameObject Player
    20.     {
    21.         get { return aiBlackboard.GetDataValue<GameObject>("Player"); }
    22.         set { aiBlackboard.SetDataValue("Player", value); }
    23.     }
    24.     // I want to do what I do above here, add the new player object to a list of GameObjects
    25.  
    26.     public List<GameObject> Players = new List<GameObject>();
    27.     private EventManager<ServerAIEvent> AIManager;
    28.  
    29.  
    30.     void Awake()
    31.     {
    32.                 //THIS NEEDS TO BE REFERENCED BEFORE THE PROPERTIES
    33.         aiBlackboard = Blackboard.FindBlackboardWithName("AI Test Behaviour_Blackboard");
    34.         Debug.Log("Awake Called" + aiBlackboard);
    35.         AIHealth = 100;
    36.  
    37.     }
    38.  
    39.     void AddNewPlayerObj(GameObject newPlayer)
    40.     {
    41.         //Add the new Player's Object to the list
    42.         Players.Add(newPlayer);
    43.         Player = newPlayer;
    44.  
    45.         AIHealth -= 10;
    46.         Debug.Log("Added New Player Obj: " + newPlayer);
    47.     }
    48.  
    49.     void DeleteExistingPlayerObj(GameObject oldPlayer)
    50.     {
    51.         Players.Remove(oldPlayer);
    52.  
    53.         AIHealth += 10;
    54.         Debug.Log("Deleted Old Player Obj: " + oldPlayer);
    55.     }
    56.  
    57.     void AddListeners()
    58.     {
    59.         AIManager = EventManagerRepository.Instance.GetSingleInstanceEventManager<ServerAIEvent>();
    60.         AIManager.AddListener<GameObject>(ServerAIEvent.SpawnNewPlayer, AddNewPlayerObj);
    61.         AIManager.AddListener<GameObject>(ServerAIEvent.DespawnNewPlayer, DeleteExistingPlayerObj);
    62.     }
    63.  
    64.     void RemoveListeners()
    65.     {
    66.         AIManager.RemoveListener<GameObject>(ServerAIEvent.SpawnNewPlayer, AddNewPlayerObj);
    67.         AIManager.RemoveListener<GameObject>(ServerAIEvent.DespawnNewPlayer, DeleteExistingPlayerObj);
    68.     }
    69.  
    70.     void OnEnable()
    71.     {
    72.         AddListeners();
    73.     }
    74.  
    75.     void OnDisable()
    76.     {
    77.         RemoveListeners();
    78.     }
    79. }
    80.  
    Another thing. This script won't add to the editor prior to the server being launched or ran. It won't because of a null reference error:

    Code (csharp):
    1. NullReferenceException: Object reference not set to an instance of an object
    2. AIBlackboardVariables.get_AIHealth () (at Assets/Scripts/AIBlackboardVariables.cs:14)
    What appears clear to me is the properties I have set up aren't finding the Blackboard I am trying to get in Awake. How do I go about doing this to stop this error so I can set up the script in the visual editor normally? If I run the server, open the behavior tree and then link the start node to this script, It works perfectly fine. Also the AIHealth variable on the blackboard using this method outside of the NC visual editor is completely wrong (either on the Behavior object or the actual AI object) - I assume this is a cause of this issue.

    What I want to happen: When the event (in Addlisteners) of the player spawning another character, to add that GameObject into the GameObjet list in the blackboard. When player's object is deleted, remove it from this list. I know if I spent some time messing around with it more I could probably come to some sort of conclusion and figure it out, but it is my bedtime and I figure I mine as well pick your brain about it so I don't have to worry about it and can progress on tomorrow.

    On a completely offtopic note, do people's avatars disappear for anyone else when you login to the site? I can only see them when I am logged off and find that very strange.
     
  43. Ecocide

    Ecocide

    Joined:
    Aug 4, 2011
    Posts:
    293
    Hi, the last sentence kind of summarizes what I need and what PM can't provide, yes :)
     
  44. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Generaly speaking, yes BT nodes have one input :) While NC can allow for multiple node inputs, it would create more problems than it would solve in the case of Behaviour Trees. For example, let's suppose that you have a Sequencer and a single Action node connected twice to that sequencer. The Action will execute but it will not be reseted until the BT's cycle is finished, so when the Sequencer executes the Action again (from second output) it will immediately return instead of executing.
    Moreover it may lead down to a spaggeti graph :)

    Thinking about this a bit through, and considering that node and action is different I might be able to pull off Action Condition nodes to reference the same Action or Condition Task from an original 'reference node'. Need some more thought on that, but seems easy and interesting to do :)

    By the way, you can dublicate a node with Control + D right now.

    Regarding the UI points, Indeed both are bugs. The first was kind of a known bug that slipped away, while the second something I completely missed. Thanks. I will fix them ASAP.

    Hello and thanks!
    Glad to know that :)
    Let's see:

    Within an Action Task you already have a reference of the blackboard that is assigned on the 'Behaviour Tree Owner'. So unless you want to read and write from/to a different blackboard than that one, you don't have to work on getting that reference like you do. Also using BBVariables will make things a bit easier. Here I have altrered your script to do what you want it to do, but considering that the blackboard you read/wrtie is the same as the one assigned on the BehaviourTreeOwner:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using NodeCanvas;
    6. using NodeCanvas.Variables;
    7.  
    8. [ScriptCategory("Blackboard")]
    9. public class AIBlackboardVariables : ActionTask {
    10.  
    11.     public BBFloat AIHealth;
    12.     public BBGameObject Player;
    13.     public BBGameObjectList Players = new BBGameObjectList{blackboardOnly = true};
    14.  
    15.     private EventManager<ServerAIEvent> AIManager;
    16.  
    17.     void Awake()
    18.     {
    19.         AIHealth.value = 100;
    20.     }
    21.  
    22.     void AddNewPlayerObj(GameObject newPlayer)
    23.     {
    24.         Players.value.Add(newPlayer);
    25.         Player.value = newPlayer;
    26.         AIHealth.value -= 10;
    27.         Debug.Log("Added New Player Obj: " + newPlayer);
    28.     }
    29.  
    30.     void DeleteExistingPlayerObj(GameObject oldPlayer)
    31.     {
    32.         Players.value.Remove(oldPlayer);
    33.         AIHealth.value += 10;
    34.         Debug.Log("Deleted Old Player Obj: " + oldPlayer);
    35.     }
    36.  
    37.     void AddListeners()
    38.     {
    39.         AIManager = EventManagerRepository.Instance.GetSingleInstanceEventManager<ServerAIEvent>();
    40.         AIManager.AddListener<GameObject>(ServerAIEvent.SpawnNewPlayer, AddNewPlayerObj);
    41.         AIManager.AddListener<GameObject>(ServerAIEvent.DespawnNewPlayer, DeleteExistingPlayerObj);
    42.     }
    43.  
    44.     void RemoveListeners()
    45.     {
    46.         AIManager.RemoveListener<GameObject>(ServerAIEvent.SpawnNewPlayer, AddNewPlayerObj);
    47.         AIManager.RemoveListener<GameObject>(ServerAIEvent.DespawnNewPlayer, DeleteExistingPlayerObj);
    48.     }
    49.  
    50.     void OnEnable()
    51.     {
    52.         AddListeners();
    53.     }
    54.  
    55.     void OnDisable()
    56.     {
    57.         RemoveListeners();
    58.     }
    59. }
    60.  
    So you will be able to set the variables from the editor.
    Please let me know if that is what you want, or you indeed want to go with getting a blackboard reference other than the one assigned on the BehaviourTreeOwner (or FSMOwner).

    If you dont want to use BBVariables, you can get the reference of the Owner blackboard within the Action or Condition Task with the 'blackboard' property. I will email you a fix to make it possible to use that in editor as well and get away of that null reference.

    I could make possible to get the last state of the FSM if that helps you :)
     
    Last edited: Apr 12, 2014
  45. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Thank you!

    This works and explains things a lot better (BB reference will got from the attached BB, you can manipulate the BB vars directly using .value) However the GameObject list still does not work as it should. It errors with:

    Code (csharp):
    1. NullReferenceException: Object reference not set to an instance of an object
    2. AIBlackboardVariables.AddNewPlayerObj (UnityEngine.GameObject newPlayer) (at Assets/Scripts/AIBlackboardVariables.cs:53)
    Which is from:

    Code (csharp):
    1. Players.value.Add(newPlayer);
    Commenting out this line allows your other BBvalues to work perfectly.

    One last note - is the behavior tree not meant to update at runtime? The only way to accurately see the BB values is by going into the visual editor. If I click on the behavior in the Scene it's values are blank and AIHealth is -10 , when it should be 100 or 90.

    Thanks!
     
  46. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Regarding the null reference. Have you selected a gameobject list variable from the dropdown of the Action Task inspector?

    $dropdown.png

    The way this works when using BB variables, is that you make a BBGameObjectList variable for example, in the task like we did. Then from the task inspector you select which variable from the blackboard it 'links' to, or in other words the .value refers to.

    If you dont want to use BB variables you can read and write to the blackboard directly using the variable's name and the 'blackboard' property which is inherited. For example:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using NodeCanvas;
    5. using NodeCanvas.Variables;
    6.  
    7. public class Test : ActionTask {
    8.  
    9.     void Awake()
    10.     {
    11.         //Initialize a blackboard gameobject list variable
    12.         blackboard.SetDataValue( "Players", new List<GameObject>() );
    13.     }
    14.  
    15.     void AddNewPlayerObj(GameObject newPlayer)
    16.     {
    17.         //Add a game object to that list
    18.         blackboard.GetDataValue<List<GameObject>>("Players").Add( newPlayer );
    19.     }
    20.  
    21.     void DeleteExistingPlayerObj(GameObject oldPlayer)
    22.     {
    23.         //Remove a game object from that list
    24.         blackboard.GetDataValue<List<GameObject>>("Players").Remove( oldPlayer );
    25.     }
    26. }
    27.  
    (Because you want to use Awake, Enable and Disable as far as I saw, and not OnExecute of the action, please use the fix I've emailed you, if you want to go this way instead of BB variables)
    I hope this last explanation didnt complicate things. If it did, you can just stick using BB variables as explained earlier :)

    The variables should of course update in runtime. It works fine here. Can you provide more information please?
    --EDIT--
    I suppose this happens to you because you haven't select the variable to 'link' to from the inspector as explained before and as such the local variable is used instead of the blackboard one (use the radio button to switch between one or the other). Let me know if that is indeed the case.
     
    Last edited: Apr 12, 2014
  47. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Wow, talk about amazing support. Got more solutions to the issue than I know what to do with. Okay so first of all yes, that was the issue, I figured this out after I read what you wrote (before the edit) that things need to be linked. I am still trying to wrap my brain around the system but it is starting to make a lot more sense.

    So it works using the first method. I got your email - I won't use this fix if I have to, I think I can get everything working right without having to resort to that for now. If I need to though I know I can fall back on this. The only problem I have now is figuring out how to structure my project. So this is what I am trying to do:

    Have multiple AI, each have different behavior trees attached. They all interact with this blackboard I have created that pools this list of game objects. Problem being the script is ran twice it seems. So obviously I am doing something wrong here but I don't know how to go about it the way you designed the system for. I have a GameObject. 'AI Test Behavior' - this holds my Behavior tree and Blackboard (this was created for me when I made my first behavior tree) What happens is when I network instantiate a object, unless I have a reference to this object (if I put the behavior tree on the object, it disappears as it doesn't exist in the scene) so I cannot use it. It seems this is the wrong way to do things though as it seems the tree is being started on this object 'AI Test Behavior' as well as instantiated object lets say 'AI(Clone)'. Before this was not happening however. What is the proper way I should be doing this?

    Also is it possible to use or reference more than one blackboard? I assume using the 'FindBlackboardWithName' you can? Is this advisable? Say I want to use the blackboard with the playerlist on every AI, but use their own blackboard on each individual AI to hold their local variables.

    Ugh sorry to be so all over the place. I think I am over complicating this. Let me tell you how it normally works in my game and what I want to do simply put:

    I have a singleton, a player database class, this holds all the players connected to the server as well as all their data. I have another singleton a AI spawner which does some things like randomly look in locations on the map and finds low populations of monsters and then spawns them accordingly. When a monster is spawned, it is network instantiated across the network. The server has a master version which contains all the logic (this one would be the one to hold the behavior tree) everyone else gets a dummy copy that mimics the server's version. What I did in the past was using my player database I would just call a method to see a player if the object that came into LOS had the tag 'Player'. It would check the player database for these objects. If it did find whoever in front of him was indeed a player, it would execute an action.

    How would I best go about setting this up in Node Canvas. Should I just write all action and condition scripts and use only my Player Database and completely skip over using the blackboard? How would you approach this?
     
  48. nuverian

    nuverian

    Joined:
    Oct 3, 2011
    Posts:
    2,087
    Hello.
    I am reading your post over and over trying very hard to understand but I am just so very confused :( Sorry.

    Let's clarify some things:
    The BehaviourTreeOwner is different than the Behaviour Tree itself. The BehaviourTreeOwner Component is attached on your game object and acts as a front end to the actual BehaviourTree. When you hit "Create" on the BheaviourTreeOwner, a BehaviourTree (named BTContainer) was created in a new game object that was placed underneath your game object. Normaly this information is transparent and you dont' fiddle with it.

    A. So you have some monsters with the BehaviourTreeOwner Component on them which all reside in the master server. When needed, you Instantiate that monster game object in the clients. If the BehaviourTreeOwner on them is set to "Execute On Start" then the BehaviourTree assigned will start when the monster is Instantiated. If it's not set to "Execute On Start", then you can start it manually by calling StartGraph() on that BehaviourTreeOwner Component. Does this work and if no why? :)

    B. Right now, you can't have BB variables that are 'linked' to different Blackboards.So the use of BB variables is best suited for 'local' BehaviourTree variables. If you definetely want to have that "Players" game object list as a blackboard variable on a kind of global blackboard, then yes, the best way to do this would be to read/write directly by getting a reference to that global blackboard with FindBlackboardWithName for example and then calling GetDataValue / SetDataValue on that one directly. On the other hand if you aren't going to use this list elsewhere within the Spawner BehaviourTree, is there a reason you want to have this list as a global blackboard variable? Since you can have that list in your singleton class, you can just use it from there.

    C. On the example Action Task you posted there is no OnExecute, nor OnUpdate, so I was thinking why you want that to be as an action at all since it doesn't do something when executed rather only does something on Awake,Enable and Disable (?). (unless you didnt post the whole script :) )

    Sorry, I know this is not a real answer, but I had a very hard time understanding your setup and what's going on :-/
     
  49. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Just a quick update. I wanted to thank Nuverian for his amazing support. Shortly after he left the reply above, he contacted me by email trying to seek further information so that he could help me come to a resolution with my issue(s). I really CANNOT stress enough how awesome his support is. I have an assets that cost 2.5x+ as much as his and I never got support anywhere NEAR as close to this before.:rolleyes:

    I ended up answering and solving my own problem. I was going about the system all wrong. For some reason the Blackboard had me stumped, puzzled and confused how it worked and how I could properly interact with it. For some reason I had it in my head with some form of determination to use the BB for all my variables for my AI. This was the wrong approach, at least for me. I instead did it the way I knew how, using my own variables and lists. What I was trying to achieve was this: Have a AI agent in a networked environment with an authoritative setup. I did this - wrote all my own scripts for all the actions I would use that utilize RPCs to send proxy agents on the client's side updates about what logic to perform. This is working great and I have successfully thrown together a mecanim animated mob that finds the nearest players, runs to him and stops and begins playing the attack animation until he moves away in which he will seek him out. Everything is in perfect sync and the visual representation Nodecanvas provides is a HUGE step up in terms of debugging and bringing in the heavy guns in getting some complex AI going.

    I dumped the lists ideas the BB variables I was using before. Instead I am accessing a static list from my player database, setting this to a local gameobject variable in the Blackboard that other nodes can access for other actions. All the actions I am using now are custom actions I have made myself and this all works beautifully. I think what trips me up is the initial learning stage. I think it could have been much easier to understand and realize some simple things I overlooked if I had a video tutorial to follow. I would have then seen all the link ups, and reasoning behind many things that I first failed to see. I know Nuverian is working on this so, it is a non issue for now :) Also I think I found a typo in your documentation here: http://nodecanvas.com/documentation/creating-custom-tasks/. The 'protected override void OnInit()' method here for a conditional task isn't right for me. I, still like an Action task, have to use string rather than void and it must be returned with null or a string.

    Anyway I am well onto my way to getting something amazing working. I will be sure to bother you with some more questions soon :) Still some things I haven't figured out I am trying to get working hehe.
     
    Last edited: Apr 14, 2014
  50. Ghosthowl

    Ghosthowl

    Joined:
    Feb 2, 2014
    Posts:
    228
    Seems Unity's site had a database error the exact minute I posted this and so it ended up double posting once it came back. Snipping this.
     
    Last edited: Apr 14, 2014