OpenFL extension in 10 steps

For one game I need ability to change screen orientation (portrait or landscape) at runtime. For Andriod and iOS targets.

I searched for ready-to-use extension, but found nothing. I’m not going to say that such extension does not exists, but I spent more than a an hour, and still found nothing.

Finally I decide to write my own.

Requirements

  • It must work on Android and iOS;
  • It must work for lime legacy (we still use legacy, because it is more stable);
  • Should work without legacy (potentially).

At first let’s gogole how to change screen orientation.

Changing screen orientation on Android

Really super simple, in one line of code:

mainActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_XXX);

Changing screen orientation on iOS #1

That’s harder. On iOS 5 and earlier you can use:

[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationXXX]

But in iOS 6 setOrientation is deprecated, you should use following code inside view controller:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return UIInterfaceOrientationXXX(interfaceOrientation);
}

Moreover, shouldAutorotateToInterfaceOrientation is deprecated in iOS 7, and you should use:

- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskXXX;
}

The one good thing in all this – we don’t need that at all 🙂

Changing screen orientation on iOS #2

After grepping through openfl and lime sources, I found:

  • Legacy mode already have such function – Stage.setFixedOrientation(Stage.OrientationXXX)
  • Non-legacy use SDL2, so we can use SDL_SetHint(SDL_HINT_ORIENTATIONS, "XXX") to set orientation

Making an Extension

To create base files for extension just run:

lime create extension NameOfYourExtension

This command will create following:

  • dependencies/ – folder for dependencies;
    • android/ – folder for android; sub-project
      • src/ – .java files;
      • org/
        • haxe/
        • extension/
          • NameOfYourExtension.java – code for android extension;
      • AndroidManifest.xml – simple android manifest;
      • build.xml – build file for android part;
      • project.properties
    • ndll/ – .ndll files will be here;
    • project/ – folder for native extension part;
      • common/ – .cpp files (you can change that later in Build.xml);
      • ExternalInterface.cpp – bridge between haxe and native code;
      • NameOfYourExtension.cpp – super simple extension example;
      • include/ – .h files (you can change that later in Build.xml);
      • Utils.h – function prototypes for NameOfYourExtension.cpp (in my point of view it should have name NameOfYourExtension.h instead of Utils.h, we can change that later);
      • Build.xml – build file for hxcpp;
    • NameOfYourExtension.hx – haxe code;
    • haxelib.json – simple config for haxelib;
    • include.xml – how extension will be included.

In my case I used EsOrientation as extension name.

Step 1

At first I moved haxe code from root package to extension.eightsines, I like when every class have package.

If you also want to do this, create following folders structure:

  • extension
    • eightsines
      • EsOrientation.hx

And modify haxe code:

package extension.eightsines; // cool package

#if cpp
    import cpp.Lib; // to use native functions
#end

// I removed loading libs for neko, because this extension is only for Android and iOS

#if android
    import openfl.utils.JNI; // to use java functions
#end

#if legacy
    import openfl.display.Stage; // to change orientation in legacy
#end

class EsOrientation {
    // Avail orientations
    public static inline var ORIENTATION_UNSPECIFIED : Int = 0;
    public static inline var ORIENTATION_PORTRAIT : Int = 1;
    public static inline var ORIENTATION_LANDSCAPE : Int = 2;

    // ... more code will be here ...

    public static function setScreenOrientation(screenOrientation : Int) : Void {
        // ... more code will be here ...
    }
}

Step 2

Let’s make part for Android.

package org.haxe.extension.esorientation; // I moved file from org.haxe.extension package to it own package

import android.content.pm.ActivityInfo;
import org.haxe.extension.Extension;

public class EsOrientation extends Extension {
    // Same values as in Haxe code.
    public static final int ORIENTATION_UNSPECIFIED = 0;
    public static final int ORIENTATION_PORTRAIT = 1;
    public static final int ORIENTATION_LANDSCAPE = 2;

    public static void setRequestedOrientation(int haxeScreenOrientation) {
        int requestedOrientation;

        // Map haxe values to android values
        switch (haxeScreenOrientation) {
            case ORIENTATION_PORTRAIT:
                requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                break;

            case ORIENTATION_LANDSCAPE:
                requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                break;

            default:
                requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
        }

        // Super simple one-liner.
        // Extension.mainActivity is reference to game activity
        Extension.mainActivity.setRequestedOrientation(requestedOrientation);
    }
}

Also I moved file from original package. If you also want to move file to different package, you should change folders structure and modify package attribute inside AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.haxe.extension.esorientation">
</manifest>

Also modify include.xml in extension folder:

<?xml version="1.0" encoding="utf-8"?>
<project>
    <!-- ... code for iOS will be added later ... -->

    <dependency name="es-orientation" path="dependencies/android" if="android" />
    <android extension="org.haxe.extension.esorientation.EsOrientation" />
</project>

Step 3

Let’s link android code with haxe code.

// below last "public static inline var ..."

#if android
    private static var setRequestedOrientationNative = JNI.createStaticMethod(
        "org.haxe.extension.esorientation.EsOrientation", // fully qualified class name
        "setRequestedOrientation", // method name
        "(I)V" // JNI method signature
    );
#end

...

// inside setScreenOrientation()
#if android
    setRequestedOrientationNative(screenOrientation);
#end

That’s all 🙂 Android part should work now. I really like how quick and simple you can integrate android code with haxe code.

P. S. More info about JNI method signatures

Step 4

As legacy mode doesn’t require any code (spoiler: that’s lie), just write Haxe part:

public static function setScreenOrientation(screenOrientation : Int) : Void {
    #if android
        setRequestedOrientationNative(screenOrientation);
    #elseif legacy
        switch (screenOrientation) {
            case ORIENTATION_PORTRAIT:
                Stage.setFixedOrientation(Stage.OrientationPortraitAny);

            case ORIENTATION_LANDSCAPE:
                Stage.setFixedOrientation(Stage.OrientationLandscapeAny);

            default:
                Stage.setFixedOrientation(Stage.OrientationAny);
        }

    #end
}

Step 5

Android part should work well for both legacy and non-legacy modes, but iOS part require different approach.

At first, open .h file and change original “int SampleMethod(int inputValue)” to something we need:

#ifndef ES_ORIENTATION_H
#define ES_ORIENTATION_H

namespace EsOrientation {
    #if defined(IPHONE) // I added guard #if for iphone
    void setRequestedOrientation(int screenOrientation);
    #endif
}

#endif

Than write real function in .cpp file:

#include "SDL.h"
#include "EsOrientation.h"

// Same values as in Haxe code
#define ORIENTATION_UNSPECIFIED 0
#define ORIENTATION_PORTRAIT 1
#define ORIENTATION_LANDSCAPE 2

namespace EsOrientation {
    void setRequestedOrientation(int screenOrientation) {
        if (screenOrientation == ORIENTATION_PORTRAIT) {
            SDL_SetHint(SDL_HINT_ORIENTATIONS, "Portrait");
        } else if (screenOrientation == ORIENTATION_LANDSCAPE) {
            SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
        } else {
            SDL_SetHint(SDL_HINT_ORIENTATIONS, "Portrait LandscapeLeft LandscapeRight");
        }
    }
}

And finally mofify ExtrenalInterface.cpp:

// ... includes and defines, see full code at github ...

static value es_orientation_set_requested_orientation(value screenOrientation) {
    #if defined(IPHONE)
    setRequestedOrientation(val_int(screenOrientation));
    #endif

    return alloc_null();
}

extern "C" void es_orientation_main() {
    val_int(0); // Fix Neko init
}

extern "C" int es_orientation_register_prims() {
    return 0;
}

DEFINE_PRIM(es_orientation_set_requested_orientation, 1);
DEFINE_ENTRY_POINT(es_orientation_main);

Our main function is es_orientation_set_requested_orientation. This function will be used in haxe, so it must return value. Even if you don’t want to return anything, you must return alloc_null(). Also every parameter has value type. To get real value, you must use special functions. For example to get value of type int you should use val_int().

Next you must write two magic functions – es_orientation_main and es_orientation_register_prims 🙂 I don’t really know what they do. And actually you don’t even need to write them – they already generated by lime create extension.

To be able to use C-function in Haxe, you must use DEFINE_PRIM macro. First parameter is function name, second parameter – parameters count. Out function has one parameter – so second parameter must be = 1.

Step 6

To be able to build it, you should modify Build.xml inside project folder:

<?xml version="1.0" encoding="utf-8"?>
<xml>
    <include name="${HXCPP}/build-tool/BuildCommon.xml" />
    <set name="DEBUGEXTRA" value="-debug" if="fulldebug" />

    <files id="common">
        <compilerflag value="-Iinclude" />
        <file name="src/ExternalInterface.cpp" />
    </files>

    <files id="ios">
        <compilerflag value="-Iinclude" />
        <compilerflag value="-Iinclude/SDL2" />
        <compilerflag value="-Iinclude/SDL2/configs/default" />
        <file name="src/ios/EsOrientation.mm" />
    </files>

    <target
        id="ndll"
        output="${LIBPREFIX}es_orientation${DEBUGEXTRA}${LIBEXTRA}"
        tool="linker"
        toolid="${STD_MODULE_LINK}"
    >
        <outdir name="../ndll/${BINDIR}" />
        <files id="common" />
        <files id="ios" if="ios" />
    </target>

    <target id="default">
        <target id="ndll" if="ios" unless="legacy" />
    </target>
</xml>

You shoudn’t write it from scratch, just modify Build.xml created by lime create extension.

If you don’t plan to support desktop platforms, you can safely delete <set name="SLIBEXT" value="..." if="..." /> lines and other lines related to windows / mac / linux or neko.

Also I split files to two groups – common for every platform and ios only for ios. It can be useful when support for new platrorm will be added (in far far future 🙂 ).

Also SDL.h is included to be able to use SDL functions and constants. I have SDL2 installed (via homebrew), but don’t want to put mac only include paths, so I just copy SDL headers from lime heades to my extension. I feel that this is wrong way, but don’t want to spend much time on that.

At last modify include.xml in extension root:

<project>
    <ndll name="es_orientation" if="ios" unless="legacy" />

    <dependency name="es-orientation" path="dependencies/android" if="android" />
    <android extension="org.haxe.extension.esorientation.EsOrientation" />
</project>

As usual, you shouldn’t write this from scratch, lime create extension will generate basic rules.

Step 7

Let’s link native code with haxe code:

// after JNI.createStaticMethod for android
#if (ios && !legacy)
    private static var recheckScreenOrientationNative = Lib.load(
        "es_orientation_legacy", // library name
        "es_orientation_legacy_recheck_screen_orientation", // function name
        1 // parameters count
    );
#end

...

public static function setScreenOrientation(screenOrientation : Int) : Void {
    #if android
        setRequestedOrientationNative(screenOrientation);
    #elseif legacy
        switch (screenOrientation) {
            case ORIENTATION_PORTRAIT:
                Stage.setFixedOrientation(Stage.OrientationPortraitAny);

            case ORIENTATION_LANDSCAPE:
                Stage.setFixedOrientation(Stage.OrientationLandscapeAny);

            default:
                Stage.setFixedOrientation(Stage.OrientationAny);
        }
    #elseif ios
        setRequestedOrientationNative(screenOrientation);
    #end
}

It harder than android, but still understandable.

Step 8

To build extension, register your extension with haxelib (Just use “haxelib dev …”, don’t upload maybe non-working extension to haxelib site), and than use lime rebuild name-of-extension-in-haxelib.

Let’s try!

  • Works well on Android;
  • Semi-works on iOS.

On Android screen orientation is changed immediately, but on iOS it will change screen orientation if you physically rotate the device.

I haxe zero expirience in iOS development, but I guess it happens because shouldAutorotateToInterfaceOrientation (insnde lime legacy) and supportedInterfaceOrientations (inside SDL2) was called by iOS only when user physically rotates device.

Fortunatelly, I found one extension for cordova with solution (line 65 and below). It opens and immediately closes empty view controller, and this did the trick.

Step 9

Both legacy and non-legacy modes need that trick. It is simple to copy + paste code info non-legacy code, but for legacy we need separate binary.

To do this, you should add legacy path to Build.xml:

<files id="common-legacy">
    <compilerflag value="-I_legacy/include" />
    <file name="_legacy/src/ExternalInterface.cpp" />
</files>

<files id="ios-legacy">
    <compilerflag value="-I_legacy/include" />
    <file name="_legacy/src/ios/EsOrientation.mm" />
</files>

<target
    id="ndll-legacy"
    output="${LIBPREFIX}es_orientation_legacy${DEBUGEXTRA}${LIBEXTRA}"
    tool="linker"
    toolid="${STD_MODULE_LINK}"
>
    <outdir name="../ndll/${BINDIR}" />
    <files id="common-legacy" />
    <files id="ios-legacy" if="ios"/>
</target>

Modify default target:

    <target id="default">
        <target id="ndll" if="ios" unless="legacy" />
        <target id="ndll-legacy" if="ios legacy" />
    </target>

And also modify include.xml:

<?xml version="1.0" encoding="utf-8"?>
<project>
    <ndll name="es_orientation" if="ios" unless="legacy" />
    <ndll name="es_orientation_legacy" if="ios legacy" />

    <dependency name="es-orientation" path="dependencies/android" if="android" />
    <android extension="org.haxe.extension.esorientation.EsOrientation" />
</project>

Step 10

It should work now 🙂

Link to repo with full code: haxe-es-orientation

Precompiled libraries not included, so if you want to use this extension not only for educational purposes, either compile it by yourself, or fill issue on github.

2 thoughts on “OpenFL extension in 10 steps

  1. I have walked through several tutorials like this, but this is the first that works for me. It would also be good to read how to include 3rd party SDK to an extension. Are you going to write about?

Leave a Reply to wuprui Cancel reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.