An Introduction to ShiVa iOS plugins – ShiVa Engine

An Introduction to ShiVa iOS plugins

ShiVa games run natively on a large number of platforms without the need for the developer to adjust anything. Our STK + engine architecture was designed with the "build once, run everywhere" philosophy in mind. There are times however when you want to use a feature that is native to your target platform, in which case you have to write your own plugins. If the target platform native API is coded largely in C or C++, this requires no effort. As soon as you encounter a different language like Objective-C in the Apple platforms, it becomes a much more difficult story. This tutorial will give you an introduction on how to code plugins in the Apple ecosystem and write bridge code between ShiVa and Objective-C/C++.

Method 1: Sticking to C++

The easiest way of writing ShiVa plugins for iOS is sticking to C++ and avoiding any platform-native code entirely. If the package you want to integrate in your plugin is written in C or C++, this is the best choice.
You can find the iOS plugin project in the plugin "Make" directory:

As always, you can blindly accept the suggestion Xcode throws at you to bring the project settings up to the version standards in your Xcode environment:

If you are writing to a certain C++ language specification, make sure to select the corresponding language dialect in the "Build Settings":

Note how all C++ code files do have the ".cpp" extension, supplemented by their ".h" headers.

Method 2: Calling Objective-C from C++

Writing purely in C++ will not allow you interface with native iOS calls. In order to use Objective-C and its syntax in your C++ code, you have to tell the compiler to treat your C++ code as Objective-C++, which is a superset of C++ - like Objective-C is to C. In other words, you can write C++ code in Objective-C++ files, but the C++ compiler will not understand your Objective-C++ commands.
Converting a C++ file to Objective-C++ is easy. Changing the ".cpp" file extension to ".mm" tells Xcode to compile the file with the Objective-C++ compiler. To make the process fast and easy, go to "File> New> File..." and select one of the following options. None of the choices fit our purpose perfectly, so whatever you select, you will have to make changes:
- "Objective-C File": creates an empty .h and .m file pair, which you have to rename to .h and .mm
- "C++ File": creates an empty .hpp and .cpp file pair, which you have to rename to .h and .mm
- "Cocoa Touch class": creates a skeleton .h and .m interface/implementation file pair, which you have to clear out and rename to .h and .mm


Inside the .mm file, you can now mix both C++ and Objective-C calls freely, like so:

void logVersion () {
	NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
	S3DX::log.message("Running running on version ",
                    [version cStringUsingEncoding:NSUTF8StringEncoding],
                    " build ",
                    [build cStringUsingEncoding:NSUTF8StringEncoding] );
	return;
}

This compiles and works as expected:

Common Pitfalls

While mixing ShiVa C++ and Objective-C++ code, you must be aware of a number of potential pitfalls.

Similar names, different languages

Even though C++ and Objective-C++ have similar names, they are two different languages which will be compiled independently from each other. You will need to redefine all the C++ imports you need in your .mm file in order to use them. In the case of ShiVa plugins, you will need to

#include "Plugin.h"

in order to use any S3DX:: calls.

Headers and Includes

C++ references files through the #include keyword, Objective-C uses #import. If you wish to import and Objective-C header into a C++ file, you need to encapsulate the include into a preprocessor macro:

#include "MyCppClass.h"
#include 
#ifdef __OBJC__
	#import "MyObjCCode.h"
#endif

Nil

Objective-C brings its own set of standard types. One of them in particular, NIL, conflicts with the nil type in ShiVa. Either cleanly separate ShiVa C++ code from your .mm code by not including Plugin.h in object code that depends on NIL(recommended), or replace all NILs with NULLs (lazy last resort).

Auto

Objective-C types can often not be correctly deduced by the C++11(+) "auto" keyword. Prefer declaring your Objective-C types explicitly.

Class members

It is technically possible to use Objective-C typed members in your C++ class. As with #imports, you have to encapsulate those in an __OBJC__ macro:

class CCM {
private:
	const char * _sEventMotion 	= "onCMMotion";
#ifdef __OBJC__
	NSTimeInterval			_objcCMPollingRate = 1.0/60.0;
	CMMotionManager			* _objcCMM;
	NSOperationQueue 		* _objcCMQueue;
#endif
public:
	CCM();
	~CCM();
	void engineEventLoop();
};

Otherwise you will get unknown type errors:

Method 3: Bridge Classes

By far the most common scenario you will face is this: You found a 3rd party SDK for iOS, or want to integrate new iOS native core functionality into ShiVa, but those are written in Objective-C and make use of Interfaces, Implementations, Delegates, Protocols and all the other things that cannot be used directly in C++. To make these SDKs available in ShiVa, you have to design a bridge class.
A bridge class consists of 2 halves, a C++ side and an Objective-C++ side, both in .mm files. We can create the missing .h/.mm pair like before by creating a Cocoa Touch Class from the New File dialogue, however this time our entries matter, since we will actually use the skeleton code produced by Xcode. Choose a name for your bridge class (must be different from the C++ class), and inherit fron NSObject unless you have good reasons to do something else.

This will leave us with 4 files (2x .h, 2x .mm). In this example, "BridgeClass.*" is the mostly-C++ class, and "BridgeClass_o.*" is the mostly-Objective-C++ class.

After applying the include/member variable rules from above, all we need is a way for these two classes to interact. One convenient way to do this is via class pointers. The C++ header file carries the "(id)" of the Objective-C class instance as a member variable:

//
//  C++ header
//  BridgeClass.h
#pragma once
class BridgeClass {
private:
	// target AI name
	const char * _sAI = "game";
	// pointer to the bridge ObjC-Class
#ifdef __OBJC__
	id _objcClass;
#endif
public:
	// constructor/destructor
	BridgeClass();
	~BridgeClass();
	// user plugin functions
	bool init (const char * sAI);
};

Likewise, the Objective-C class lists the BridgeClass pointer as one of its properties:

//
//  Objective-C++ header
//  BridgeClass_o.h
#pragma once
// C++ ShiVa plugin include
#include "Plugin.h"
// include C++ Bridge class header
#include "BridgeClass.h"
// Objective-C Class
@interface BridgeClass_o : NSObject
@property BridgeClass * pointerToCpp;
@end

As soon as the C++ class is constructed, it allocates momory for the Objective-C class counterpart and stores its (id) inside the member variable. Directly after that, the class pointer "this" is sent to Objective-C class through the automatically synthesized function "setPointerToCpp" which stores the result in the "pointerToCpp" @property. Since we are not using Apple ARC, we must make sure to destroy every Objective-C object ourselves by releasing it at the appropriate time:

//
//  C++ class implementation
//  BridgeClass.mm
#include "BridgeClass.h"
#import "BridgeClass_o.h"
// constructor/destructor
BridgeClass::BridgeClass() {
	_objcClass = NULL;
	_objcClass = [[BridgeClass_o alloc] init];
	[_objcClass setPointerToCpp:this];
}
BridgeClass::~BridgeClass() {
	[_objcClass release];
}

The init function inside the Objective-C class implementation is as simple as it can get. Note how the keyword "this" is changed to "self" in Objective-C, but otherwise does practically the same:

//
//  Objective-C Implementation
//  BridgeClass_o.mm
#import "BridgeClass_o.h"
@implementation BridgeClass_o
- (id)init
{
	self = [super init];
	if (self) {
	//initialize something here if needed
	}
	return self;
}
@end

With a connection like this established, you can now call C++ class member functions from Objective-C and vice versa. For instance, here is how you would invoke the "BridgeClass::logMessageFromObjC" method from the Objective-C function "createAlert":

// Objective-C
- (BOOL)createAlert {
	if (!_pointerToCpp)
		return false;
	// auto-syntehsized underscore prefix!
	_pointerToCpp->logMessageFromObjC("This is a log from ObjC -> C++");
	return true;
}

The other direction makes use of the [] syntax as we have seen in method 2. For instance, here is how you would invoke the "createAlertWithString" method from the Objective-C class behind (id)_objcClass and transmit a single string argument:

bool BridgeClass::createAlert (const char * _sMessage){
	if (_objcClass == nullptr)
		return false;
	[_objcClass createAlertWithString:_sMessage];
	return true;
};

Example: Using a native iOS alert popup

As a practical example, I want to show how to use a native iOS framework (UIKit) and pop up a standard alert on the screen with a couple of buttons. First, we need to include the header of the framework we want to use:

// BridgeClass_o.h Objective-C++ header
// for alert box
#import 

Next, we need to add a class method which allows ShiVa to call into our Objective-C class:

// BridgeClass_o.h Objective-C++ header
- (BOOL)createAlertWithString: (const char *) fromShiVa;

The implementation looks as follows:

// BridgeClass_o.mm Objective-C++ implementation
- (BOOL)createAlertWithString: (const char *) fromShiVa {
	S3DX::log.message("ObjC: message received!");
	S3DX::log.message(fromShiVa);
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ShiVa Alert"
                    message:[NSString stringWithUTF8String:fromShiVa]
                    delegate:self
                    cancelButtonTitle:@"Cancel"
                    otherButtonTitles:@"Choice", @"Another one",  NULL];
	[alert show];
	// Not using ARC? Release alert view
	[alert release];
	return true;
}

UIAlertView is long deprecated, but it still works and allows me to easily show another useful Objective-C feature you will need to deal with in your plugins: delegates.

Delegates

Delegates can be thought of as quite similar to ShiVa handlers. When you send a message (user.sendMessage()/object.sendMessage()), you expect your target AI to have an event handler with a certain number of arguments which processes your message. Similarly, UIAlertView sends messages to a delegate with certain predefined functions using a protocol.
In the code above, we defined the delegate to be "self", which means we have to implement the delegate functions (our "event handlers") in the BridgeClass_o.mm file:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
	S3DX::log.message("Alert button #", (float)buttonIndex, "pressed!");
}

"alertView" and its prototype are predefined by UIKit and will get called automatically when a button belonging to (UIAlertView *) is clicked.
It is customary to declare the delegate protocol on the interface using angled brackets:

@interface BridgeClass_o : NSObject <UIAlertViewDelegate>
...
@end

The result of the code looks like this:

All log messages are visible in the Xcode debugger:


  • slackBanner