playMaker

Author Topic: Custom action with asynchronous callback  (Read 5316 times)

pt5011

  • Playmaker Newbie
  • *
  • Posts: 6
Custom action with asynchronous callback
« on: July 13, 2017, 02:42:07 AM »
Hi everyone!
New Playmaker user here. So please bear with me :)

I'm familiar enough with Unity and C# scripting, but very new to Playmaker. I'm creating custom actions for some of my existing scripts. Everything went fairly well, until I hit a certain method that receives an asynchronous callback.

Say I have this fictitious method

Code: [Select]
UserData.RetrieveAge(string username, System.Action<int> callback)
which retrieves the user's age from a remote server, with the callback being invoked once the server responds. How should I design my action to properly handle this? The first thing came to my mind is something like this
Code: [Select]
public FsmString username;
public FsmInt userAge;
public FsmEvent callbackEvent;

public override void OnEnter()
{
UserData.RetrieveAge(username.Value, MyCallback);
Finish();
}

void MyCallback(int age)
{
userAge.Value = age;
Fsm.Event(callbackEvent);
}

Is this a proper design? I'm afraid not, thinking of 2 scenarios:

1. Since the action finishes right after the call to UserData.RetrieveAge, if I make a transition upon FINISHED and move to a next state, then MyCallback will never be invoked because the state became inactive (am I right here?).

2. If I remove the Finish() call, the FSM will stay at the current state waiting for MyCallback to be invoked and send the callbackEvent event, which may never happen due to network/server issues. In that case the FSM will get stuck at the current state forever (?)

To my understanding, the ideal approach would be to have the action finish right after the RetrieveAge call, and somehow have the callback send a global event/transition. The question is how to achieve that in Playmaker?

What is your common design pattern for methods like this? Sorry if I miss something obvious.

Thanks for reading!

tcmeric

  • Beta Group
  • Hero Member
  • *
  • Posts: 768
Re: Custom action with asynchronous callback
« Reply #1 on: July 13, 2017, 03:10:11 AM »
There is no need to call finished(), if you are using a playmaker event to move to the next state.

Once the FSMevent is triggered (assuming the person has set up the event in playmaker), it will just go to the next playmaker state.

If you did need some kind of clean up, you can use
public override void OnExit()
{
}

You might want to set it up with an if statement, checking if the callback is successful. You could have another FSMevent for failure. That might help with debugging, or not let your game hang.

djaydino

  • Administrator
  • Hero Member
  • *****
  • Posts: 7615
    • jinxtergames
Re: Custom action with asynchronous callback
« Reply #2 on: July 13, 2017, 06:07:27 AM »
Hi,
Something like this :

Code: [Select]
// (c) Copyright HutongGames, LLC 2010-2013. All rights reserved.
// Made by : Me

using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("MyData")]
    [Tooltip("Destroys a Game Object.")]
public class MyDataGetAge : FsmStateAction
{
[RequiredField]
[Tooltip("The User Name")]
public FsmString userName;

[Tooltip("send event when succeeded.")]
public FsmEvent callbackEvent;

        [Tooltip("send event when failed.")]
        public FsmEvent failureEvent;

        [UIHint(UIHint.Variable)]
        [Tooltip("Store the value of the userAge")]
        public FsmInt userAge;

        private int age = 0;

        public override void Reset()
        {
            userName = null;
            callbackEvent = null;
            failureEvent = null;
            userAge = null;
        }



public override void OnEnter()
{
            age = UserData.RetrieveAge(userName.Value);
           

            if (age == 0)
            {
                Fsm.Event(failureEvent);
            }
            else
            {
                userAge.Value = age;
                Fsm.Event(callbackEvent);
            }
        }
    }
}

You should also always use :

Code: [Select]
public override void Reset()
        {
            userName = null;
            callbackEvent = null;
            failureEvent = null;
            userAge = null;
        }
To prevent wrong values when the action is used more than once.

A good way is to look at the action scripts that are included with Playmaker and custom actions which you can find on the Ecosystem

pt5011

  • Playmaker Newbie
  • *
  • Posts: 6
Re: Custom action with asynchronous callback
« Reply #3 on: July 14, 2017, 11:48:18 PM »
Thank you all for your replies!

However, it looks like you misunderstood my question. So let's me explain it again.

Suppose I have this action MyDataGetAge, and in its OnEnter() I make a call to the RetrieveAge method, which in turn makes a request to a remote server. Note that RetrieveAge doesn't return an age immediately. Rather, it registers a callback, which will be invoked when the server returns the actual age.

Now, I could drop the Finish(), and let the action standing there waiting until the server responds, and the callback is invoked. Then in the callback handler I'd store the returned age into a FsmInt and send a FsmEvent to trigger some transition and move to the next state. The problem is, if the server never responded (due to network issues, for instance), then the callback and the FsmEvent would never be triggered, causing the fsm to be stuck at the MyDataGetAge action forever.

What I want to achieve is to have the action finishes right after the RetrieveAge call (right after sending a request to the server), so the fsm can move to the next state if it so desires. Then when the server actually responds, somehow invoke the callback (even if the MyDataGetAge action is now inactive), and in the callback send a FsmEvent, which can be wired to some global transition.

Hope I've been clearer this time.

Thank you very much. I look forward to hearing your replies.

tcmeric

  • Beta Group
  • Hero Member
  • *
  • Posts: 768
Re: Custom action with asynchronous callback
« Reply #4 on: July 14, 2017, 11:59:03 PM »
I think the most simple non-technical solution (and djaydino may have a better one than this, so bare with me  :) ), is to add a time out to it then.

1. If it doesnt get a response by X time (maybe you can allow the user to set this via a variable), then the action ends. If this is just simply for you and don't need a perfect solution, you can add a wait action to the same state as your custom action. Once the wait action finishes, it will trigger, moving you to the next state. So you can use this as your time out. (If you action finishes first, it can trigger an event to move to another state).

2. Also, I would suggest running it on its own FSM. That way it doesnt stop the rest of the FSM from functioning. Since it is a state machine, only one state can be running at a time. A general way to get around this, is just by using 2 FSMs running at the same time. Then the same action can keep running in the background until it gets the results. Once it does, you can have it send a global event to your main FSM, or set a variable in the main FSM to pass the necessary data.

pt5011

  • Playmaker Newbie
  • *
  • Posts: 6
Re: Custom action with asynchronous callback
« Reply #5 on: July 15, 2017, 11:27:32 PM »
@tcmeric Thank you for your help, especially the tip on using a 2nd fsm.

This is not only for my own use, I have a little plugin on the Asset store, and some of my customers were asking me to add Playmaker actions for it, and that's why :)

Your workarounds sound like they will work. The use of a 2nd fsm is a neat one, provided that my customers know about it!

I do look for a "perfect solution", hopefully djaydino and other members will have it.

djaydino

  • Administrator
  • Hero Member
  • *****
  • Posts: 7615
    • jinxtergames
Re: Custom action with asynchronous callback
« Reply #6 on: July 16, 2017, 12:45:37 AM »
Hi,
I am still learning a lot from C# but i will try :)

How are you doing the timeout in your script?

or when someone uses :
Code: [Select]
UserData.RetrieveAge(username.Value, MyCallback);in his script?

pt5011

  • Playmaker Newbie
  • *
  • Posts: 6
Re: Custom action with asynchronous callback
« Reply #7 on: July 17, 2017, 10:46:19 AM »
Hi,

Actually, there's no such timeout, because RetrieveAge doesn't wait. It just sends a request to the server, register the callback, then...done, the execution moves on to the next instruction. If the server responds, the callback will be invoked, otherwise, it won't. That's how it works. I want to achieve a similar workflow with Playmaker actions.

Thanks.

djaydino

  • Administrator
  • Hero Member
  • *****
  • Posts: 7615
    • jinxtergames
Re: Custom action with asynchronous callback
« Reply #8 on: July 17, 2017, 02:17:00 PM »
Hi,
I did something similar but i was getting info from a www and i used a Coroutine for this.

Here is a sample from the code :

Code: [Select]
// (c) Copyright HutongGames, LLC 2010-2016. All rights reserved.
// Made by Djaydino http://www.jinxtergames.com
// Works with http://dreamlo.com/
/*--- __ECO__ __PLAYMAKER__ __ACTION__ ---*/

using UnityEngine;
using System.Collections;

namespace HutongGames.PlayMaker.Actions
{
[ActionCategory("Web")]
[Tooltip("Saves highscore to Dreamlo website")]
public class DreamloGetScoreToString : FsmStateAction

    {
public enum SaveType
{
xml,
json,
pipe,
quote
}

public enum SortType
{
ascending,
descending
}

public enum SortBy
{
score,
seconds,
date
}

[ActionSection("Code")]

[RequiredField]
[Tooltip("Place Public Code from the Dreamlo website")]
public FsmString publicCode;

[ActionSection("Data")]

[Tooltip("saves your list into this string)")]
[UIHint(UIHint.Variable)]
public FsmString highscoreList;

[Tooltip("choose what type you wish to save into)")]
public SaveType saveType;

[ActionSection("Sort")]



[Tooltip("here you can choose the size of your list for example 10 will give the top 10 scores, leave to none or 0 to get all scores")]
public FsmInt ListSize;


public SortType sortType;

public SortBy sortBy;

[ActionSection("Error")]

public FsmEvent errorEvent;
[Tooltip("Where any errors thrown will be stored. Set this to a variable, or leave it blank.")]

public FsmString errorMessage = "";

private WWW www;

        private Coroutine routine;
private string webUrl;
private string setSaveType;

        public override void Reset()
        {
saveType = SaveType.xml;
sortType = SortType.descending;
sortBy = SortBy.score;
publicCode = null;
highscoreList = null;
ListSize = null;
errorMessage = "";
        }

        public override void OnEnter()
        {
switch (saveType)
{
case SaveType.xml:
setSaveType = "xml";
break;

case SaveType.json:
setSaveType = "json";
break;

case SaveType.pipe:
setSaveType = "pipe";
break;

case SaveType.quote:
setSaveType = "quote";
break;
}

switch (sortBy)
{
case SortBy.score:
break;

case SortBy.seconds:
setSaveType = setSaveType + "-seconds";
break;

case SortBy.date:
setSaveType = setSaveType + "-date";
break;
}

switch (sortType)
{
case SortType.descending:
break;

case SortType.ascending:
setSaveType = setSaveType + "-asc";
break;
}


if (ListSize == null || ListSize.Value == 0)
{
webUrl = "http://dreamlo.com/lb/" + publicCode.Value + "/" + setSaveType;

}
else
{
webUrl = "http://dreamlo.com/lb/" + publicCode.Value + "/" + setSaveType + "/" + ListSize;

}
routine = StartCoroutine(UploadNewHighscore());
        }

        private IEnumerator UploadNewHighscore()
        {
www = new WWW(webUrl);
            yield return www;

if (string.IsNullOrEmpty(www.error))
{
highscoreList.Value = www.text;
Finish();
}
else
{
errorMessage = www.error;
Fsm.Event(errorEvent);
}

        }

        public override void OnExit()
        {
            StopCoroutine(routine);
        }
    }
}
Maybe this is the way to go?

pt5011

  • Playmaker Newbie
  • *
  • Posts: 6
Re: Custom action with asynchronous callback
« Reply #9 on: July 17, 2017, 11:24:39 PM »
Thanks for your help.

Unfortunately, my RetrieveAge method is not a coroutine like your UploadNewHighscore, and I'm afraid it's not ideal/possible to rewrite it into a coroutine.

I think the key here is whether the action waits for the server response or not. If I understand it correctly, your action is waiting in the coroutine until the upload completes (which may be necessary in your case). My RetrieveAge method doesn't wait for the server, and therefore doesn't block execution.

I've just found this old post http://hutonggames.com/playmakerforum/index.php?topic=10401.msg49084#msg49084, which seems relevant. They're talking about using some kind of proxies, which is alien to me :)

Is that the correct approach? If yes, how do we create/use proxies in Playmaker? As that post suggested, I'm going to dig into the Photon code, which is quite a lot :|, so I'd much appreciate if you can give me a general idea about proxies first.

Many thanks!




djaydino

  • Administrator
  • Hero Member
  • *****
  • Posts: 7615
    • jinxtergames
Re: Custom action with asynchronous callback
« Reply #10 on: July 18, 2017, 01:20:23 AM »
Hi,
I do not know what your script is/do but maybe you may approach this differently.

maybe have an action to initiate you script to get stuff from the server,

then have an action to get the result values from your script.

so on the action you would simply have something like this :

Code: [Select]
namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("my Category")]
    [Tooltip("do something")]
public class myAssetGetAge : FsmStateAction
{
[RequiredField]
[UIHint(UIHint.Variable)]
public FsmInt age;
public FsmEvent Done;
        public FsmEvent Error;


        public override void Reset()
{
            age = null;
            Done = null;
            Error = null;
}

public override void OnEnter()
{


if (Yourscript.yourVariable == -1) //in your script you could set this, if nothing is retrieved age is set to -1 (or maybe you can use isnone or sometihng)
            {
                Fsm.Event(Error);
                Finish();
            }
            else
            {
                age.Value = Yourscript.yourVariable;
                Fsm.Event(Done);
                Finish();
            }
}
}
}

for initiate you can use something like this :

Code: [Select]
Yourscript.YourFunction();

tcmeric

  • Beta Group
  • Hero Member
  • *
  • Posts: 768
Re: Custom action with asynchronous callback
« Reply #11 on: July 18, 2017, 01:32:01 AM »
I would say, dont over think it.

1. Your action makes the call.
2. Wait a few seconds using the wait in the same state.

Next state
3. Check to see if the results are null or not.
4. If null, send to next state to wait a few seconds and try again.
5. If not null, send to another state because all is good.

To me, this is the playmaker way. Trying to program every situation into one action is more the C# way, rather than the playmaker way :)

tcmeric

  • Beta Group
  • Hero Member
  • *
  • Posts: 768
Re: Custom action with asynchronous callback
« Reply #12 on: July 18, 2017, 01:39:02 AM »
If you want to do it in c#.

Maybe

1. Make the callback, and check for results. If no results, run coroutine.
2. Put coroutine in a loop.
3. Run coroutine and wait 10 seconds. Check results, If no results, run loop again.
4. Keep looping as many times as you want. (Can set as a variable. Both the loop count and the wait amount). Expose to playmaker.
5. Once loop count is reached, send the a playmaker event like "results null".
6. If call back is returned and no long null, script can return and send the playmaker event for "no null".

djaydino

  • Administrator
  • Hero Member
  • *****
  • Posts: 7615
    • jinxtergames
Re: Custom action with asynchronous callback
« Reply #13 on: July 18, 2017, 01:40:30 AM »
Hi,
@tcmeric
For you own use that would be fine.

But if you want to place it on the asset store, it is important to make it as simple as possible for the customer. ;)

tcmeric

  • Beta Group
  • Hero Member
  • *
  • Posts: 768
Re: Custom action with asynchronous callback
« Reply #14 on: July 18, 2017, 03:00:22 AM »
 8) 8) 8)