Calling into SkookumScript directly from C++


#1

So far I have BP<->Sk calls, Sk->C++ calls, and C+±>BP->Sk calls by way of BlueprintNativeEvents. Everything’s working great. I’m just curious if there’s currently a mechanism for C+±>Sk calls without the need for BP wrappers? Is this still the way to go?

Cheers and thanks.

Edit:

I’m unable to test this currently, but if I make a BlueprintNativeEvent tagged C++ function and I call that without having implemented it in BP will the C++ implementation be called as it is in the absense of :sk: or would the Sk implementation be called?


#2

Sounds like you have a great start!

If you want to call from C++ into SkookumScript, just use the :sk: C++ API directly. Get access to a SkInstance object and call either:

  • method_call() - multi argument version
  • method_call() - 0/1 argument version
  • method_query() - 0/1 argument version returning true/false
  • coroutine_call() - multi argument version
  • coroutine_call() - 0/1 argument version

These can all be found in SkInstance.hpp. Many of the comments for the SkookumScript C++ API methods are just above the method bodies in the .cpp files. We’ve moved some of the comments to the .hpp header files such as with the method_call() and method_query() though the coroutine_call() comments are still in the .cpp so their comments are here for reference. We’ll eventually move the method comments to the header files so that they are all visible in our free SkookumScript API.

//---------------------------------------------------------------------------------------
// Evaluates the method with 0 or more arguments and returns immediately
// 
// Params:
//   method_name: name of method to call
//   args_pp:
//     Optional pointers to object instances to use as arguments - each one present
//     should have its reference count incremented and each defaulted/skipped argument
//     should be a `nullptr` element. If `arg_count` is 0 this is ignored
//   arg_count:
//     number of arguments to use in `args_pp`. If it is 0 then no arguments are passed
//     and `args_pp` is ignored.
//   caller_p:
//     object that called/invoked this expression and that may await a result.  If it is
//     nullptr, then there is no object that needs to be notified when this invocation is
//     complete.
//   result_pp:
//     Pointer to a pointer to store the instance resulting from the invocation of this
//     expression.  If it is nullptr, then the result does not need to be returned and
//     only side-effects are desired.
//     
// Notes:
//   This is a convenience method to use instead of SkMethodCall<>::invoke_call() - if more arguments
//   or control is desired, then use SkMethodCall<>::invoke_call()
//   
// See:
//   method_call(), method_query(), SkMethodCall<>::invoke_call(), call_destructor(),
//   call_default_constructor()
virtual void method_call(
  const ASymbol & method_name,
  SkInstance **   args_pp,
  uint32_t        arg_count,
  SkInstance **   result_pp = nullptr,
  SkInvokedBase * caller_p = nullptr
  );
//---------------------------------------------------------------------------------------
// Evaluates the method with 0/1 arguments and returns immediately
// 
// Params:
//   method_name: name of method to call
//   arg_p:
//     Optional pointer to object instance to use as an argument.  If it is present it
//     should have its reference count incremented.  If it is nullptr, then no arguments
//     are passed.
//   caller_p:
//     object that called/invoked this expression and that may await a result.  If it is
//     nullptr, then there is no object that needs to be notified when this invocation is
//     complete.
//   result_pp:
//     Pointer to a pointer to store the instance resulting from the invocation of this
//     expression.  If it is nullptr, then the result does not need to be returned and
//     only side-effects are desired.
//     
// Notes:
//   This is a convenience method to use instead of `method_call(name, args_pp, --)`
//   - if more arguments or control is desired, then use it instead
//   
// See:
//   method_call(), method_query(), SkMethodCall<>::invoke_call(), call_destructor(),
//   call_default_constructor()
void method_call(
  const ASymbol & method_name,
  SkInstance *    arg_p = nullptr,
  SkInstance **   result_pp = nullptr,
  SkInvokedBase * caller_p = nullptr
  );
//---------------------------------------------------------------------------------------
// Evaluates the method with 0/1 arguments and returns a Boolean `true` or
// `false` result immediately.
// 
// Returns: the result of the method call as `true` or `false`.
// 
// Params:
//   method_name: name of method to call
//   arg_p:
//     Optional argument to be passed to method.  If it is nullptr, then no arguments are
//     passed.
//   caller_p:
//     Object that called/invoked this expression and that may await a result.  If it is
//     `nullptr`, then there is no object that needs to be notified when this invocation
//     is complete.
// 
// Notes:
//   This is a convenience method to use instead of `method_call(name, args_pp, --)`
//   - if more arguments or control is desired, then use it instead
// 
// See: method_query(), SkMethodCall<>::invoke_call(), call_destructor(), call_default_constructor()
bool method_query(
  const ASymbol & method_name,
  SkInstance *    arg_p    = nullptr,
  SkInvokedBase * caller_p = nullptr
  );
//---------------------------------------------------------------------------------------
// Evaluates the coroutine call with 0 or more arguments.
// 
// Returns:
//   `nullptr` if the coroutine completed immediately or an invoked coroutine if the
//   coroutine has a deferred completion.
//   
// Params:  
//   coroutine_name:
//     name of the coroutine to call if it exists for this object. If the specified
//     coroutine does not exist for this object it will assert if `(SKOOKUM & SK_DEBUG)`
//     is set.
//   args_pp:
//     Optional pointers to object instances to use as arguments - each one present should
//     have its reference count incremented and each defaulted/skipped argument should be
//     a `nullptr` element.  If arg_count is 0 this is ignored
//   arg_count:
//     number of arguments to use in args_pp.  If it is 0 then no arguments are passed and
//     args_pp is ignored.
//   immediate:
//     if true the coroutine is invoked immediately (it may not be completed, but it will
//     be *started* immediately), if false the coroutine is scheduled for invocation on
//     the next update.
//   update_interval:
//     Specifies how often the coroutine should be updated in seconds.
//     (Default SkCall_interval_always)
//   caller_p:
//     object that called/invoked this expression and that may await a result - call its
//     `pending_deferred()` method with the result of this method as necessary.  If it is
//     `nullptr`, then there is no object that needs to be notified when this invocation
//     is complete.
//   updater_p:
//    Mind object that will update this invoked coroutine as needed - generally same
//    updater as the caller.  If nullptr the caller's updater is used and if the caller is
//    nullptr scope_p is used.
SkInvokedCoroutine * coroutine_call(
  const ASymbol & coroutine_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
  );
//---------------------------------------------------------------------------------------
// Evaluates the coroutine call with 0/1 arguments.
// 
// Returns:
//   `nullptr` if the coroutine completed immediately or an invoked coroutine if the
//   coroutine has a deferred completion.
//   
// Params:  
//   coroutine_name:
//     name of the coroutine to call if it exists for this object. If the specified
//     coroutine does not exist for this object it will assert if `(SKOOKUM & SK_DEBUG)`
//     is set.
//   arg_p:
//     pointer to an object to use as an argument to the coroutine. If it is nullptr then
//     no argument is passed.
//   immediate:
//     if true the coroutine is invoked immediately (it may not be completed, but it will
//     be *started* immediately), if false the coroutine is scheduled for invocation on
//     the next update.
//   update_interval:
//     Specifies how often the coroutine should be updated in seconds.
//     (Default SkCall_interval_always)
//   caller_p:
//     object that called/invoked this expression and that may await a result - call its
//     `pending_deferred()` method with the result of this method as necessary.  If it is
//     `nullptr`, then there is no object that needs to be notified when this invocation
//     is complete.
//   updater_p:
//    Mind object that will update this invoked coroutine as needed - generally same
//    updater as the caller.  If nullptr the caller's updater is used and if the caller is
//    nullptr scope_p is used.
SkInvokedCoroutine * coroutine_call(
  const ASymbol & coroutine_name,
  SkInstance *    arg_p,           // = nullptr
  bool            immediate,       // = true
  f32             update_interval, // = SkCall_interval_always
  SkInvokedBase * caller_p,        // = nullptr
  SkMind *        updater_p        // = nullptr
  );

Definitely ask on the forum if you have more SkookumScript C++ API questions.


Binding SkookumScript to own C++ (UE4)
Executing SK coroutines from UE
C++ to Skookum calling
SkookumScript API for UE4
Great curated posts to learn about SkookumScript
About Automated tests using SK
#3

Awesome. Getting direct calls will save a lot of time. BP’s great, but wrapping all those functions takes time.

I’ve managed to get SkookumScript/SkInstance.hpp included in my gamemode header, but I’m having trouble with including SkookumScriptComponent.h. I’d like to use the component to have easy access to the SkInstance.

I know I can add the component in BP and use a call into BP to get it but I’d like to guarantee that the component is there without having to necessarily extending my gamemodes with BP.

In SkTest.Build.cs I added "SkookumScript" to PublicDependencyModuleNames and added PublicIncludePaths.AddRange(new string[] { "SkookumScript/Public"});. This gave me access to SkInstance.hpp.

I’m having trouble getting access to the Component though.

Is adding the component as a default possible? Is it safe/recommended?

Edit: Actually I wrote that before trying to get the instance through BP. Looks like I can’t do it that way.


#4

The component is in SkookumScriptRuntime and SkookumScriptRuntime/Classes. I got a cyclic dependency error after adding that to my project though. Maybe we need to create a new module that depends on both :sk: and our main game module? I’ll report back when I have the chance to try that!


#5

In my post above I forgot to mention I tried that.

I added SkookumSkriptRuntime/Classes to the PublicIncludePaths and everything was ok.

I ran into the same cyclical dependency problem when I tried to put SkookumScriptRuntime into the SkTest.Build.cs.

Unfortunately I’ve yet to mess with modules in :ue4:. I was just proud of myself for even figuring out how to get access to SkInstance :wink:


#6

Try adding to PrivateDependencyModuleNames instead of PublicDependencyModuleNames. Also just the dependency on the module should be enough, there should be no need to also add to PublicIncludePaths or PrivateIncludePaths. Then #include SkookumScript/SkInstance.hpp should work.

With SkookumScriptComponent, if, after adding SkookumScriptRuntime to your PrivateDependencyModuleNames, #include "SkookumScriptComponent.h" does not work, try moving SkookumScriptComponent.h from Classes to Public and see if that fixes the issue.

I will look deeper into this later today.


#7

With SkookumScript and SkookumScriptRuntime in PrivateDependencyModuleNames, I still get this when I try to compile:

1>------ Build started: Project: UE4, Configuration: BuiltWithUnrealBuildTool Win32 ------
2>------ Build started: Project: BandK, Configuration: Development_Editor x64 ------
2>  Creating makefile for hot reloading BandKEditor (no existing makefile)
2>  Compiling game modules for hot reload
2>  Parsing headers for BandKEditor
2>    Running UnrealHeaderTool "W:\Projects\BandK-UE\BandK\BandK.uproject" "W:\Projects\BandK-UE\BandK\Intermediate\Build\Win64\BandKEditor\Development\UnrealHeaderTool.manifest" -LogCmds="loginit warning, logexit warning, logdatabase error" -Unattended -WarningsAsErrors -installed
2>  Reflection code generated for BandKEditor in 47.1013489 seconds
2>EXEC : error : Action graph contains cycle!
2>
2>  Action #10: C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\../../VC/bin/amd64\link.exe
2>  	with arguments:  /MANIFEST:NO /NOLOGO /DEBUG /errorReport:prompt /MACHINE:x64 /SUBSYSTEM:WINDOWS /FIXED:No /NXCOMPAT /STACK:5000000 /DELAY:UNLOAD /DLL /PDBALTPATH:%_PDB% /OPT:NOREF /OPT:NOICF /INCREMENTAL /ignore:4199 /ignore:4099 /LIBPATH:"W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Source\SkookumScript\Lib\Win64\VS2015" /NODEFAULTLIB:"LIBCMT" /NODEFAULTLIB:"LIBCPMT" /NODEFAULTLIB:"LIBCMTD" /NODEFAULTLIB:"LIBCPMTD" /NODEFAULTLIB:"MSVCRTD" /NODEFAULTLIB:"MSVCPRTD" /NODEFAULTLIB:"LIBC" /NODEFAULTLIB:"LIBCP" /NODEFAULTLIB:"LIBCD" /NODEFAULTLIB:"LIBCPD" @"W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Intermediate\Build\Win64\UE4Editor\Development\UE4Editor-SkookumScriptRuntime-3640.dll.response" /OUT:"W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Binaries\Win64\UE4Editor-SkookumScriptRuntime-3640.dll" /IMPLIB:"W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Intermediate\Build\Win64\UE4Editor\Development\UE4Editor-SkookumScriptRuntime-3640.lib" /PDB:"W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Binaries\Win64\UE4Editor-SkookumScriptRuntime-3640.pdb" /ignore:4078
2>  	depends on: W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Intermediate\Build\Win64\UE4Editor\Development\SkookumScriptRuntime\Module.SkookumScriptRuntime.cpp.obj
2>  	depends on: W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Intermediate\Build\Win64\UE4Editor\Development\SkookumScriptRuntime\SkookumScriptRuntime.generated.cpp.obj
2>  	depends on: W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Intermediate\Build\Win64\UE4Editor\Development\SkookumScriptRuntime\PCLaunch.rc.res
2>  	depends on: W:\Projects\BandK-UE\BandK\Plugins\SkookumScript\Intermediate\Build\Win64\UE4Editor\Development\SkookumScriptRuntime\ModuleVersionResource.rc.inl.res

And a couple hundred more lines like that, whether SkookumScriptComponent.h is in Classes or is in Public

I got it to work with a different approach though. The caveat is that it involves modifying the plugin.

Instead of adding SkookumScriptRuntime to the dependencies, I created an interface in my project like so:

#pragma once

#include "HasSkInstance.generated.h"

class SkInstance;

/**
* Interface for an object that has an SkInstance
*/
UINTERFACE()
class UHasSkInstance : public UInterface
{
	GENERATED_BODY()
};

class BANDK_API IHasSkInstance
{
	GENERATED_BODY()

   public:
	virtual SkInstance* GetSkInstance() const = 0;
};

Then I opened SkookumScriptComponent.h and changed the declaration to this:

class SKOOKUMSCRIPTRUNTIME_API USkookumScriptComponent : public UActorComponent, public AListNode<USkookumScriptComponent>, public IHasSkInstance // Note the added "public IHasSkInstance"

And inserted this the body:

	  // Begin IHasSkInstance interface
	  virtual SkInstance* GetSkInstance() const override
	  {
		  return get_sk_instance();
	  }
	  // End IHasSkInstance interface

Finally, I went back to the class where I want to call :sk: methods and I added:

	const auto Components = GetComponents();

	IHasSkInstance* ComponentWithSkInstance = nullptr;

	for (const auto Component : Components)
	{
		ComponentWithSkInstance = Cast<IHasSkInstance>(Component);
		if (ComponentWithSkInstance) break;
	}
	
	if (!ComponentWithSkInstance)
	{
		UE_LOG(LogTemp, Error, TEXT("%s has no SkookumScriptComponent."), *GetName());
            return;
	}

	const auto SkInstance = ComponentWithSkInstance->GetSkInstance();
	check(SkInstance);

	// We Shoud really use a symbol constant here instead of creating an ASymbol on the fly.
	// See the comment of ASymbol::create
	SkInstance->method_call(ASymbol::create(TEXT("on_primary_mouse_press")));

#8

Hmm interesting. I think the proper solution would be to break the SkookumScriptRuntime module into two modules to resolve the dependency cycle. I’ll think about this more and get back to you.


#9

Has this been resolved? I want to be able to call Skookum from C++ and currently the documentation points to this thread, which seems to be unresolved.


#10

It depends on where and how you are calling into SkookumScript. In most cases calling from C++ into Sk works fine.

The above post talks about having a cyclic include error.

Try it out and see how you fare with respect to includes.

If there still is a cyclic include issue and you get it too, @GreatGuru might be able to take a look at this tomorrow.


#11

Great, thank you, I’ll try playing around with it.


#12

Newbie question: What’s the proper way for a C++ file to get access to a SkInstance, as mentioned above?


#13

I’m having trouble with this too. Have you had any luck Scott?


#14

I ended up working around it by using C++ to call a blueprint and have a blueprint call Skookum. This seemed much easier for me to figure out, even if it was roundabout.

There are examples of calling from C++ -> Skookum directly in the Firebug project, I believe.


#15

That’s what I’m doing currently as well. I made a blueprint native event and planned on just using the _implementation function in cpp to make the call when I figured out how to call directly.