Executing SK coroutines from UE


#1

This is a variation on other questions but with a twist. I’ve decided to ditch my generic AI behavior tree code and put all my AI logic into SK coroutines. Pretty standard stuff so far. The twist is that I want all my actors’ attributes to be changeable dynamically and data driven. For example, an NPC could be a close combat fighter, a ranged weapon user, or a magic user. Think the old Neverwinter Nights style of choosing an NPC’s behavior in combat. I would have a routine for each type, and a pointer to that routine would be a variable in the actor. I also want the player to be able to choose which routine they want to use, and I want that set of choices to be dynamically generated so that later on I can mod the game to add new behaviors. For my junior programmers, I want to be able to give them a template and then let them develop new behaviors. Ultimately, all the AI is going to be SK code.

So the questions are a) how do I create a pointer to a SK coroutiine in C++ (or Blueprint if necessary) which can be updated via code? B) How do I structure the SK code to do something like this? I’m thinking of creating a class hierarchy just for my AI, but I’d like to hear the guru’s ideas on what would be the SK way.


#2

Sounds cool.

Here are some initial ideas.

Calling closures from Sk

You might be able to do a bunch of what you want directly in SkookumScript using closures.

This would be easy to data-drive and be quite easy to test and iterate.

Just have data members for each behavior category and then specific behaviors slotted in as closures.

You can see more about closures in the SkookumScript online reference docs.

Closures can be pretty sophisticated, so please feel free to ask more questions about them here on the forum.

Calling Sk coroutines from C++

if you want to drive things from C++, then that is also no problem.

To start things off, there are some helper C++ methods for calling Sk methods and coroutines:

These C++ calls do not cache the pointer to the routine - they look it up each time which can be a cost you don’t want. Is that the case? We can follow up with some more complicated C++ code that describes how to cache a Sk coroutine (your original question). :madsci:


#3

Here is (a somewhat simplified version of) the C++ code body for SkInstance::call_coroutine() mentioned in the forum post above. It is somewhat looking under the hood though we are happy if you want to dive into the C++ side of things and get more fancy. I’ve added some Note: sections in the comments.

// Note: If not interested in all these arguments then simplify as needed.
SkInvokedCoroutine * SkInstance::coroutine_call(
  const ASymbol & coroutine_name,  // cache or use a constant for the name
  SkInstance **   args_pp,
  uint32_t        arg_count,
  bool            immediate,       // = true
  f32             update_interval, // = SkCall_interval_always
  SkInvokedBase * caller_p,        // = nullptr
  SkMind *        updater_p        // = nullptr
  )
  {
  // Find coroutine - assumes it will be found
  // Note: Could cache this if you know it will be the same on successive calls.
  SkCoroutineBase * coroutine_p = m_class_p->find_coroutine_inherited(coroutine_name);

  // Initialize invoked coroutine
  SkInvokedCoroutine * icoroutine_p = SkInvokedCoroutine::pool_new(coroutine_p);
  icoroutine_p->reset(update_interval, caller_p, this, updater_p);

  // Note: These SKDEBUG_ macros interact with the Sk debugging system - they are
  // not necessary though they are probably good to have.
  SKDEBUG_ICALL_SET_INTERNAL(icoroutine_p);
  SKDEBUG_HOOK_SCRIPT_ENTRY(coroutine_name);

  // Fill invoked coroutine's argument list
  icoroutine_p->data_append_args(args_pp, arg_count, coroutine_p->get_params());

  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Call immediately or register coroutine to be called in next script update
  // Note: you will probably use one or the other so simplify the other mechanism out
  if (immediate)
    {  // Call immediately
    if (icoroutine_p->on_update())
      {
      SKDEBUG_HOOK_SCRIPT_EXIT();
      return nullptr;
      }
    }
  else
    {
    // Append to coroutine update list
    icoroutine_p->m_mind_p->coroutine_track_init(icoroutine_p);
    }

  SKDEBUG_HOOK_SCRIPT_EXIT();
  return icoroutine_p;
  }

If the coroutine doesn’t return immediately (and what interesting coroutine does?) then the SkInvokedCoroutine object will be used in successive calls of the coroutine until it completes. The invoked coroutine carries around the runtime info needed including arguments, its call stack, etc. It can also be suspended, checked to see if it is waiting on other sub-calls, etc.

If you do want to keep the invoked coroutine around, then use a AIdPtr<SkInvokedCoroutine> smart pointer that will automatically return NULL once the invoked coroutine has completed.

This is all fairly advanced stuff so please ask for any clarification that you need if you dive in the deep end here.

Good luck!