Mobile and cross-platform games

Various Android applications developed using Castle Game Engine

Mobile platforms (Android, iOS) differ from desktop platforms (Windows, Linux, Mac OS X, FreeBSD...) in many aspects. Our engine hides a lot of these differences from you by giving you a nice cross-platform API. Still, there are some facts you have to know when developing a game for a mobile platform (or developing a game both for mobile and standalone platforms).

1. Compiling and debugging

Developing for mobile platforms requires installing some special tools. Everything is explained on these platform-specific pages:

Compiling and packaging cross-platform games is greatly simplified if you use our build tool. For Android and iOS, our build tool nicely hides from you a lot of complexity with creating a platform-specific application.

  • For Android, you get a ready working xxx.apk file.
  • For iOS, you get a ready project that can be installed using XCode.

Rendering on mobile platforms uses OpenGLES. Our OpenGLES renderer can handle almost everything the same as a desktop OpenGL renderer, but there are some things not implemented yet in the OpenGLES version.

2. Create a platform-independent Game unit

It is easy to create a game that can be compiled both for Android/iOS and for desktop. To make it work, you should create a simple platform-independent main game unit (traditionally called just game.pas in our example projects) and maintain a very small platform-specific game program/library files.

See the skeleton project in engine examples castle_game_engine/examples/portable_game_skeleton/ for a skeleton code using this approach. Feel free to use it as a start of your projects. Other examples of this approach can be found in most new projects/examples of our engine. For example see castle_game_engine/examples/2d_spine_game/ code. Or Darkest Before the Dawn source code (see game.pas of Darkest Before the Dawn).

The idea is to:

  1. Define main (platform-independent) game code in a unit like game.pas (and of course use other units, as needed).

  2. The initialization section of the Game unit should only assign callbacks to Application.OnInitialize, and create window and assign it's callbacks. Do not do any real processing in the unit initialization, e.g. do not load any game data (because this is too early, for example Android activity is not started at this point yet).

    Actual game initialization (loading images, resources, setting up player and such) should happen in the callback implementing Application.OnInitialize. At that point you know that your program is ready to load resources (e.g. Android activity really started). Also, you know that the first OpenGL context is just created, so you can freely use things that require OpenGL context, and show loading progress, like SceneManager.LoadLevel.

    The initialization must assign the Applcation.MainWindow instance, that will be used by platform-specific program/library code. It should descend from TCastleWindowCustom class (in most cases, just use the standard TCastleWindow or TCastleWindowTouch classes, although you can also derive your own window classes).

    {$mode objfpc}{$H+}
     
    { Implements the game logic, independent from mobile / standalone. }
    unit Game;
     
    interface
     
    uses CastleWindowTouch;
     
    var
      Window: TCastleWindowTouch;
     
    implementation
     
    uses SysUtils, CastleWindow, CastleScene, CastleControls, CastleLog,
      CastleFilesUtils, CastleSceneCore, CastleKeysMouse, CastleColors;
     
    var
      ExampleImage: TCastleImageControl;
      ExampleScene: TCastleScene;
     
    { routines ------------------------------------------------------------------- }
     
    { One-time initialization of resources. }
    procedure ApplicationInitialize;
    begin
      { This is just an example of creating a simple 2D control
        (TCastleImageControl) and 3D object (TCastleScene). }
     
      ExampleImage := TCastleImageControl.Create(Window);
      ExampleImage.URL := ApplicationData('example_texture.png');
      ExampleImage.Bottom := 100;
      ExampleImage.Left := 100;
      Window.Controls.InsertFront(ExampleImage);
     
      ExampleScene := TCastleScene.Create(Application);
      ExampleScene.Load(ApplicationData('example_scene.x3dv'));
      ExampleScene.Spatial := [ssRendering, ssDynamicCollisions];
      ExampleScene.ProcessEvents := true;
      Window.SceneManager.Items.Add(ExampleScene);
      Window.SceneManager.MainScene := ExampleScene;
    end;
     
    procedure WindowRender(Container: TUIContainer);
    begin
      // ... custom rendering code
      UIFont.Print(10, 10, Yellow, Format('FPS: %f', [Container.Fps.RealTime]));
    end;
     
    procedure WindowUpdate(Container: TUIContainer);
    begin
      // ... do something every frame
    end;
     
    procedure WindowPress(Container: TUIContainer; const Event: TInputPressRelease);
    begin
      // ... react to press of key, mouse, touch
    end;
     
    function MyGetApplicationName: string;
    begin
      Result := 'my_fantastic_game';
    end;
     
    initialization
      { This sets SysUtils.ApplicationName.
        It is useful to make sure it is correct (as early as possible)
        as our log routines use it. }
      OnGetApplicationName := @MyGetApplicationName;
     
      InitializeLog;
     
      { initialize Application callbacks }
      Application.OnInitialize := @ApplicationInitialize;
     
      { create Window and initialize Window callbacks }
      Window := TCastleWindowTouch.Create(Application);
      Application.MainWindow := Window;
      Window.OnRender := @WindowRender;
      Window.OnUpdate := @WindowUpdate;
      Window.OnPress := @WindowPress;
    end.
  3. In the Game unit, do not:

    • Do not call Window.Open or Window.Close or Application.Run.
    • Do not create more than one TCastleWindowCustom instance. If you want your game to be truly portable to any device — you have to limit yourself to use only one window. For normal games that's probably natural anyway.

      Note that the engine still supports, and will always support, multiple-window programs, but for that you will have to just write your own program code. See e.g. castle_game_engine/examples/window/multi_window.lpr example. There's no way to do it portably, for Android, iOS, web browser plugin...

  4. Create a CastleEngineManifest.xml file to compile your project using the build tool. It can be as simple as this:

    <?xml version="1.0" encoding="utf-8"?>
    <project name="my-cool-game" game_units="Game">
    </project>

    Compile and run it on your desktop using this on the command-line:

    castle-engine compile
    castle-engine run

    If you have installed Android SDK, NDK and FPC cross-compiler for Android then you can also build and run for Android:

    castle-engine package --os=android --cpu=arm
    castle-engine install --os=android --cpu=arm
    castle-engine run --os=android --cpu=arm

    If you have installed FPC cross-compiler for iOS then you can also build for iOS:

    castle-engine package --target=iOS
    # And open in XCode the project inside
    # castle-engine-output/ios/xcode_project/
    # to compile and run on device or simulator.
  5. Optionally, to be able to run and debug the project from Lazarus, you can also create a desktop program file like my_fantastic_game_standalone.lpr. It may be as simple as this:

    {$mode objfpc}{$H+}
    {$apptype GUI}
    program my_fantastic_game_standalone;
    uses CastleWindow, Game;
    begin
      Application.MainWindow.OpenAndRun;
    end.

    You can even generate a simple program skeleton (lpr and lpi files) using

    castle-engine generate-program

    Desktop .lpr file can do some more useful stuff, for example initialize window to be fullscreen, read command-line parameters, and load/save user configuration. See examples how to do it: darkest_before_dawn program file (simple) or hotel_nuclear (more complicated).

    Note that you can edit and run the desktop version using Lazarus, to benefit from Lazarus editor, code tools, integrated debugger... Using our build tool does not prevent using Lazarus at all!

    • If you did not create the lpi file using castle-engine generate-program, you can create it manually: Simply create in Lazarus a new project using the New -> Project -> Simple Program option. Or (if you already have the xxx.lpr file) create the project using Project -> New Project From File....
    • Add to the project requirements packages castle_base and castle_window (from Project -> Project Inspector, you want to Add a New Requirement).
    • Save the project as my_fantastic_game_standalone.lpi.
    • ...and develop and run as usual.
    • Edit the main my_fantastic_game_standalone.lpr file using the Project -> View Project Source option in Lazarus.

3. Prepare for differences in input handling

To create portable games you have to think about different types of inputs available on mobile platforms vs desktop. The engine gives you various helpers, and abstracts various things (for example, mouse clicks and touches can be handled using the same API, you just don't see multi-touches on desktop). But it's not possible to hide the differences 100%, because some concepts just cannot work — e.g. mouse look cannot work on touch interfaces (since we don't get motion events when you don't press...), keyboard is uncomfortable on touch devices, multi-touch doesn't work on desktops and so on.

A simple example of a 3D navigation, if you use Player instance (see manual chapter about the Player):

{ 1. Declare a trivial global variable that controls whether
  input is touch (with multi-touch, without keyboard etc.) or desktop
  (without multi-touch, with keyboard etc.).
 
  Using a variable to toggle desktop/touch input,
  that can even be changed at runtime, is useful in Michalis experience
  --- it allows to somewhat debug touch input on desktops,
  by just setting DesktopCamera to false, e.g. when a command-line option
  like --touch-device is given. }
 
var
  DesktopCamera: boolean =
    {$ifdef ANDROID} false {$else}
    {$ifdef iOS}     false {$else}
                     true {$endif} {$endif};
 
{ 2. Then, somewhere where you initialize the game
     (probably after SceneManager.LoadLevel?) initialize the input.
 
     The code below assumes that you initialized Player, and you assigned
     SceneManager.Player := Player. This way we can be sure that current
     camera (SceneManager.Camera) is equal to player
     (SceneManager.Camera = Player.Camera). }
 
if DesktopCamera then
begin
  Player.Camera.MouseLook := true;
end else
begin
  Window.AutomaticTouchInterface := true;
  { Above will automatically set Window.TouchInterface based on
    current navigation mode (walk, fly..).
    It will also set camera MouseDragMode, which is very useful
    in combination with some touch interface modes.
    To actually enable the dragging, you still need to do this: }
  Player.EnableCameraDragging := true;
end;