NAV Navbar
JavaScript C++

Getting Started

Download the Source Code

First, make sure you've installed Git as that's what we'll be using to retrieve the source code. The following command will download Rainbow and all of its submodules into a folder named rainbow:

git clone --recursive https://bitbucket.org/tido/rainbow.git

If you're using a graphical UI for Git, make sure to checkout the submodules as well.

Building for Windows

  1. Install Visual Studio Community.

  2. Install CMake.

  3. Optional: Download FMOD Studio API, and extract it. Copy the headers into lib/FMOD/inc/, and binaries into lib/FMOD/lib/windows/. You should end up with the following:

    rainbow/lib/FMOD
    ├── inc
    │   ├── fmod.h
    │   ├── fmod.hpp
    │   ├── fmod_codec.h
    │   ├── fmod_common.h
    │   ├── fmod_dsp.h
    │   ├── fmod_dsp_effects.h
    │   ├── fmod_errors.h
    │   ├── fmod_output.h
    │   ├── fmod_studio.h
    │   ├── fmod_studio.hpp
    │   └── fmod_studio_common.h
    └── lib
        └── windows
            ├── fmod.dll
            ├── fmod_vc.lib
            ├── fmod64.dll
            ├── fmod64_vc.lib
            ├── fmodstudio.dll
            ├── fmodstudio_vc.lib
            ├── fmodstudio64.dll
            └── fmodstudio64_vc.lib
    
  4. The PowerShell script tools\make.ps1 will walk you through customising your Rainbow project. Find it using File Explorer, right-click the file, and select Run with PowerShell. You can dismiss any prompts about execution policy.

  5. When Visual Studio opens the project, simply hit F5 to build and run.

Building for macOS

  1. Install Xcode from App Store.

  2. Once Xcode is installed, launch it once to install Command Line Tools.

  3. Install Homebrew.

  4. Install build dependencies from the root of the repo: brew bundle

  5. Optional: Download FMOD Studio API, extract it, and point tools/fmod-import-tool.sh at the directory you extracted it to. This should copy all the necessary files into the lib folder.

  6. Make a directory for building Rainbow, then run the build script:

    mkdir rainbow-build
    cd rainbow-build
    /path/to/rainbow/tools/build.sh -DCMAKE_BUILD_TYPE=<configuration> [option ...]
    
  7. Start the playground:

    ./rainbow /path/to/rainbow/js
    

You'll find an overview of configuration and feature flags further below.

Building for Linux

  1. Use your distro's package manager to install the dependencies:

    Arch Linux pacman -S --needed cmake gcc mesa ninja pulseaudio sdl2
    Debian/Ubuntu apt install build-essential cmake libgl1-mesa-dev libpulse-dev libsdl2-dev ninja pkg-config pulseaudio
  2. Optional: Download FMOD Studio API, extract it, and point tools/fmod-import-tool.sh at the directory you extracted it to. This should copy all the necessary files into the lib folder.

  3. Make a directory for building Rainbow, then run the build script:

    mkdir rainbow-build
    cd rainbow-build
    /path/to/rainbow/tools/build.sh -DCMAKE_BUILD_TYPE=<configuration> [option ...]
    
  4. Start the playground:

    ./rainbow /path/to/rainbow/js
    

You'll find an overview of configuration and feature flags further below.

Building for Android

  1. Install Android Studio.
  2. Install CMake.
  3. Optional: Download FMOD Studio API, extract it, and point tools/fmod-import-tool.sh at the directory you extracted it to. This should copy all the necessary files into the lib folder.
  4. Open build/android/ in Android Studio.
  5. Run the app on your device or emulator.

Building for iOS

  1. Install Xcode from the App Store.
  2. Open build/ios/Rainbow.xcodeproj in Xcode.
  3. Build and run the current scheme.

Build Configuration

Available features and build configurations:

Build configuration Description
Debug Compiles Rainbow in debug mode with little or no optimizations.
Release Compiles Rainbow in release mode with most optimizations turned on.
RelWithDebInfo Similar to Release but with debugging symbols.
MinSizeRel Similar to Release with emphasis on small binary size.
Feature flag Description
UNIT_TESTS Compiles unit tests. Only useful for engine developers.
USE_FMOD_STUDIO Replaces Rainbow's custom audio engine with FMOD Studio.
USE_HEIMDALL Compiles in Rainbow's debug overlay and other debugging facilities.
USE_PHYSICS Compiles in Box2D.
USE_SPINE Enables support for loading Spine rigs.

Example: Build Rainbow with physics and Spine support for game development.

/path/to/rainbow/tools/build.sh -DCMAKE_BUILD_TYPE=Debug \
                                -DUSE_HEIMDALL=1 \
                                -DUSE_PHYSICS=1 \
                                -DUSE_SPINE=1

You can pass any number of CMake options.

Specifying Compiler

By default, GCC and Clang will be used to compile both C and C++ code on Linux and macOS respectively. Unix Makefiles is the default generator on Linux, and Xcode on macOS. You can change these by prefixing environment variables. For instance, to use GCC and Ninja in place of Clang and Makefiles:

CC=gcc CXX=g++ GENERATOR=Ninja /path/to/rainbow/tools/build.sh [option ...]

If you have problems running build.sh, make sure it has execution permission:

chmod +x /path/to/rainbow/tools/build.sh

Basics

File Structure

config.json (optional)

The configuration file is just a JSON. It enables/disables features, and sets the window size (Linux/macOS/Windows).

{
  "$schema": "./rainbow-config.schema.json",
  "accelerometer": true,
  "allowHighDPI": false,
  "msaa": 0,
  "resolution": {
    "width": 1280,
    "height": 720
  },
  "suspendOnFocusLost": true
}

Entry Point

// Any class that derives from `GameBase` is a potential entry point. The class
// may also override any or none of the methods `init_impl()` and
// `update_impl()`. Although, not overriding any of them will display nothing.

class MyGame final : public rainbow::GameBase
{
public:
    MyGame(rainbow::Director& director) : rainbow::GameBase(director) {}

private:
    rainbow::spritebatch_t batch_;

    void init_impl(const Vec2i &screen) override;
    void update_impl(unsigned long dt) override;
};

// The engine will call `init_impl()` once on startup, after a graphics context
// has been created. From then on, `update_impl()` will be called every frame.
// `dt` is the time passed since last frame, in milliseconds.
//
// Finally, let Rainbow know which class to use by implementing
// `GameBase::create()`:

auto rainbow::GameBase::create(rainbow::Director& director)
    -> std::unique_ptr<rainbow::GameBase>
{
    return std::make_unique<MyGame>(director);
}

// See `Script/NoGame.cpp` and `Script/NoGame.h` for a complete example. We will
// add sprites to the screen in the following sections.
// 'index.js' must implement the following two entry-point functions:

function init(width, height) {
  // Called once on startup.
}

function update(dt) {
  // Called every frame. |dt| is the time since last frame in milliseconds.
}

// There is no draw function. Later, we'll use the render queue to get things
// onto the screen.

Sprite Batches

One of Rainbow's philosophies is to always batch sprites. So in order to create a sprite, one must first create a batch.

void MyGame::init_impl(const Vec2i& screen)
{
    constexpr unsigned int count = 2;  // Intended number of sprites in batch
    batch_ = rainbow::spritebatch(count);
function init(screenWidth, screenHeight) {
  const count = 2;  // Intended number of sprites in batch
  const batch = new Rainbow.SpriteBatch(count);

The count tells Rainbow that we intend to create a batch of two sprites. Next, we'll create two sprites:

    auto sprite1 = batch_->create_sprite(100, 100);
    auto sprite2 = batch_->create_sprite(100, 100);
  const sprite1 = batch.createSprite(100, 100);
  const sprite2 = batch.createSprite(100, 100);

The order in which sprites are created are important as it determines the draw order. Here, sprite1 is drawn first, followed by sprite2.

Now we have a sprite batch containing two untextured sprites. Let's add a texture:

    // Load the texture atlas.
    auto atlas = rainbow::texture("canvas.png");
    batch_->set_texture(atlas);

    // Create a texture from the atlas, and assign to our sprites.
    auto texture = atlas->create(Vec2i{448, 108}, 100, 100);
    sprite1->set_texture(texture);
    sprite2->set_texture(texture);
  // Load the texture atlas.
  const atlas = new Rainbow.Texture("canvas.png");
  batch.setTexture(atlas);

  // Create a texture from the atlas, and assign to our sprites.
  const texture = atlas.addRegion(448, 108, 100, 100);
  sprite1.setTexture(texture);
  sprite2.setTexture(texture);

First, we load the actual texture. Textures are always loaded into atlases which can be assigned to sprite batches. "Actual" textures are created by defining a region. These, in turn, are assigned individual sprites. This makes the texture atlas and its textures reusable. Additionally, it enables skinning, i.e. changing the texture of every sprite in a batch by changing only the texture atlas. Rainbow does not prevent you from loading the same asset.

Please refer to the API reference for full details. For displaying text, look up Label.

Render Queue

Anything that needs to be updated and/or drawn every frame, must be added to the render queue. The render queue determines the order in which objects are drawn.

Now we'll add the batches we've created earlier:

    // Add batch to the render queue. Note that the render queue is only
    // accessible from the entry point. If you need it elsewhere, you must pass
    // along its pointer.
    auto unit = render_queue().emplace_back(batch_);

    // Position our sprites at the center of the screen.
    const float cx = screen.x * 0.5f;
    const float cy = screen.y * 0.5f;
    sprite1->set_position(Vec2f{cx - 50, cy});
    sprite2->set_position(Vec2f{cx + 50, cy});
}
  // Add batch to the render queue.
  const unit = Rainbow.RenderQueue.add(batch);

  // Position our sprites at the center of the screen.
  const cx = screenWidth * 0.5;
  const cy = screenHeight * 0.5;
  sprite1.setPosition({ x: cx - 50, y: cy });
  sprite2.setPosition({ x: cx + 50, y: cy });
}

If you compile and run this code, you should see two identical sprites next to each other at the center of the screen.

As always, refer to the API reference for full details.

Scenes

Sometimes, dealing with the render queue can be confusing or frustrating if you can't fully visualise the order. However, you can define entire scenes with JSON. An empty scene looks something like:

{
  "$schema": "./rainbow-scene.schema.json",
  "assets": [
    /* declare fonts, sounds or textures here */
  ],
  "entities": [
    /* declare animations, labels or sprites here */
  ]
}

We can recreate the earlier scene:

{
  "$schema": "./rainbow-scene.schema.json",
  "assets": [
    {
      "id": "atlas",
      "path": "canvas.png",
      "regions": [[448, 108, 100, 100]]
    }
  ],
  "entities": [
    {
      "id": "batch",
      "texture": "atlas",
      "sprites": [
        {
          "id": "sprite1",
          "width": 100,
          "height": 100,
          "position": {
            "x": 0,
            "y": 0
          },
          "texture": 0
        },
        {
          "id": "sprite2",
          "width": 100,
          "height": 100,
          "position": {
            "x": 0,
            "y": 0
          },
          "texture": 0
        }
      ]
    }
  ]
}

If you save the file as my.scene.json, we can implement our new entry point:

void MyGame::init_impl(const Vec2i& screen)
{
    scene_ = std::make_unique<rainbow::Scene>("my.scene.json");
    render_queue().emplace_back(scene_);

    // You can also access assets and resources through this object. We retrieve
    // them by name:

    // auto batch = scene_->get<SpriteBatch>("batch");
    // auto sprite1 = scene_->get<SpriteRef>("sprite1");
}
function init(screenWidth, screenHeight) {
  scene = new Rainbow.Scene("my.scene.json");
  Rainbow.RenderQueue.add(scene, "myScene");

  // Position our sprites at the center of the screen.
  const cx = screenWidth * 0.5;
  const cy = screenHeight * 0.5;
  scene.sprite1.setPosition({ x: cx - 50, y: cy });
  scene.sprite2.setPosition({ x: cx + 50, y: cy });
}

Audio

Audio consists mainly of the sound object and the audio channel. The sound object is basically an audio buffer. It can be wholly loaded, or it can stream from disk. A sound object is played on an audio channel. An audio channel can only play one sound object at a time but the sound object can be used by any number of channels. As raw audio data can take a large amount of memory, it is recommended to only create static sound objects for short audio files (such as sound effects).

Supported Audio Codecs

OS AAC ALAC HE-AAC MP3 Ogg
Windows
macOS
Linux
Android
iOS

This table is not exhaustive. Your target devices may support more decoders than listed here. Please check the appropriate documentations.

Resource Management

Sound*  rainbow::audio::load_sound   (const char* path);
Sound*  rainbow::audio::load_stream  (const char* path);
void    rainbow::audio::release      (Sound*);
function Rainbow.Audio.loadSound(path: string): Sound;
function Rainbow.Audio.loadStream(path: string): Sound;
function Rainbow.Audio.release(sound: Sound): undefined;

Loads the audio file at given path, and returns a handle for a Sound resource if successful. Otherwise, a nullptr is returned. load_sound will load the whole file into memory, while load_stream will only open the file and stream the data as the file is played.

To release an audio resource, call release with the handle.

Playback

bool      rainbow::audio::is_paused   (Channel*);
bool      rainbow::audio::is_playing  (Channel*);
void      rainbow::audio::pause       (Channel*);
Channel*  rainbow::audio::play        (Channel*);
Channel*  rainbow::audio::play        (Sound*, Vec2f world_position = Vec2f::Zero);
void      rainbow::audio::stop        (Channel*);
function Rainbow.Audio.isPaused(channel: Channel): boolean;
function Rainbow.Audio.isPlaying(channel: Channel): boolean;
function Rainbow.Audio.pause(channel: Channel): void;
function Rainbow.Audio.play(audial: Channel | Sound): Channel;
function Rainbow.Audio.stop(channel: Channel): void;

Once a Sound resource is obtained, it can be played by calling play. This, in turn, will return a Channel handle that can be used to pause/resume/stop the playback. The handle cannot be used to restart playback if it's stopped. Once playback stops, the handle becomes invalid, and is returned to the pool to be reused by subsequent calls to play(Sound*, ...).

Configuration

void  rainbow::audio::set_loop_count      (Channel*, int count);
void  rainbow::audio::set_volume          (Channel*, float volume);
void  rainbow::audio::set_world_position  (Channel*, Vec2f position);
function Rainbow.Audio.setLoopCount(channel: Channel, count: number): void;
function Rainbow.Audio.setVolume(channel: Channel, volume: number): void;
function Rainbow.Audio.setWorldPosition(channel: Channel, position: { x: number, y: number }): void;

A currently playing channel can be further configured. Currently, you can set the number of times it should loop, its volume, and world position.

Caveats and Known Limitations

Audio channel handles are reused. This implies that an old handle may be used to manipulate a more recent playback.

Input

The input manager listens to hardware input triggered by a user and fires the appropriate events to all subscribed listeners. Such events may include key presses, mouse clicks and movements, and controller button presses.

Classes must inherit InputListener to be eligible for input event subscription. Additionally, one or more event delegates may optionally be implemented. These will be detailed later.

Input listeners are chained together and receive events in order of subscription. Delegates may return true if an event shouldn't travel further down the chain. By default, false is returned so that events reach all subscribers.

Note that we do not differentiate mouse and touch events. Both are considered pointer events.

Input Management (C++ only)

The input manager is only accessible from the entry point but you can store a pointer and pass it around. The pointer is guaranteed to be valid throughout the lifetime of your main class.

void MyGame::init_impl(const Vec2i&)
{
    Input* input_manager = &input();
    []
}
local input = rainbow.input

Subscribing

void  Input::subscribe    (InputListener& listener);
void  Input::unsubscribe  (InputListener& listener);

Subscribes/unsubscribes listener to input events. A listener will always be added to the end of the chain, regardless if it was previously subscribed. A deleted listener will automatically unsubscribe itself.

Handling Key Events

Key events come in two flavours, one for when a key is pushed down and one for when it is released. Listeners may implement any of the following methods to receive key events.

bool  InputListener::on_key_down_impl  (const Key& key) override;
bool  InputListener::on_key_up_impl    (const Key& key) override;

The key value of the event is passed as argument and, if available, its modifiers (e.g. Ctrl, Alt, or ⇧ Shift). The modifier property is a bitmask so you easily can check for multiple states. For instance, to check whether both Ctrl and ⇧ Shift is pressed, perform a bitwise OR of the two buttons and check for equality:

if (key.modifier == (Key::Mods::Shift | Key::Mods::Ctrl))
    LOGI("Ctrl+Shift is currently pressed");

Handling Mouse/Touch Events

Implement any of the following methods to receive mouse/touch events.

bool  InputListener::on_pointer_began_impl     (const ArrayView<Pointer>& pointers) override;
bool  InputListener::on_pointer_canceled_impl  () override;
bool  InputListener::on_pointer_ended_impl     (const ArrayView<Pointer>& pointers) override;
bool  InputListener::on_pointer_moved_impl     (const ArrayView<Pointer>& pointers) override;

A Pointer instance stores the location of the event, x and y, and the timestamp, timestamp, at which the event occurred. Its hash value uniquely identifies a touch, or mouse button, for the duration of it touching the screen, or mouse button being held.

The coordinate space origin is at the lower left corner, same as world space.

Example

In this example, we implement keyboard and mouse delegates. We subscribe to input events in init_impl(), and unsubscribe in the main class' destructor. While this example is running, we type "rainbow" and click on the mouse at two random places on the screen. Finally, we close the window using the keyboard shortcut Ctrl+Q/⌘Q.

#include "Input/Pointer.h"
#include "Input/VirtualKey.h"
#include "Script/GameBase.h"

class InputHandler final : public rainbow::InputListener
{
private:
    bool on_key_down_impl(const rainbow::KeyStroke& k) override
    {
        LOGI("Pressed a key: %c", rainbow::to_keycode(k.key));
        return true;
    }

    bool on_pointer_began_impl(
        const ArrayView<rainbow::Pointer>& pointers) override
    {
        for (auto&& p : pointers)
            LOGI("Pressed mouse button %u at %i,%i", p.hash, p.x, p.y);
        return true;
    }
};

class InputExample final : public rainbow::GameBase
{
public:
    InputExample(rainbow::Director& director) : rainbow::GameBase(director) {}

    ~InputExample()
    {
        // The following line is strictly unnecessary as |input_handler_| will
        // automatically unsubscribe itself on destruction.
        input().unsubscribe(input_handler_);
    }

private:
    InputHandler input_handler_;

    void init_impl(const rainbow::Vec2i&) override
    {
        input().subscribe(input_handler_);
    }
};

auto rainbow::GameBase::create(rainbow::Director& director)
    -> std::unique_ptr<rainbow::GameBase>
{
    return std::make_unique<InputExample>(director);
}

Output:

[1428763097479|INFO] Pressed a key: r
[1428763097523|INFO] Pressed a key: a
[1428763097574|INFO] Pressed a key: i
[1428763097624|INFO] Pressed a key: n
[1428763097691|INFO] Pressed a key: b
[1428763097757|INFO] Pressed a key: o
[1428763097874|INFO] Pressed a key: w
[1428763099291|INFO] Pressed mouse button 1 at 1730,978
[1428763100741|INFO] Pressed mouse button 1 at 872,686
[1428763101291|INFO] Pressed a key: 5
[1428763101461|INFO] Pressed a key: q

Sprite Sheet Animations

A sprite sheet animation is a frame-based animation (like traditional animation) where we take a sprite and change its texture at set interval.

Animation Management

Creating Sprite Sheet Animations

Animation(const SpriteRef &sprite,
          std::unique_ptr<Frame[]> frames,
          unsigned int fps,
          int delay = 0);
Rainbow.Animation(sprite: Sprite, frames: number[], fps: number, delay: number);

frames is an array of texture ids that are played back in succession. In C++, the array must be terminated with Animation::Frame::end(). The playback rate is determined by frames per second, or fps.

By default, an animation always loops without any delays between each cycle. Setting delay to anything greater than 0, will introduce delays, measured in frames. For instance, setting fps to 30 and delay to 2, will make the animation wait 66⅔ ms before playing the next cycle. A negative delay disables looping.

Before an animation can be played, it must also be added to the render queue. Batches of animations can be created and assigned a sprite at a later point in time.

Starting and Stopping Animations

bool  Animation::is_stopped  () const;
void  Animation::start       ();
void  Animation::stop        ();
function start(): void;
function stop(): void;

An animation will always start from the beginning. There is no pause function because animations live in the render queue and can therefore be paused by disabling its render unit.

unsigned int  current_frame  () const;
function currentFrame(): number;

Returns the currently displayed frame; Animation::Frame::end() (-1) if none.

unsigned int  frame_rate  () const;
function frameRate(): number;

Returns the frame rate in frames per second.

void  Animation::jump_to  (unsigned int frame);
function jumpTo(frame: number): void;

Jumps to the specified frame.

void  Animation::rewind  ();
function rewind(): void;

Rewinds the animation. Equivalent to jump_to(0).

Modifying the Animation Sequence

void  Animation::set_delay  (int delay);
function setDelay(delay: number): void;

Sets number of frames to delay before the animation loops. Negative numbers disable looping.

void  Animation::set_frame_rate  (unsigned int fps);
function setFrameRate(fps: number): void;

Sets the frame rate in frames per second.

void  Animation::set_frames  (std::unique_ptr<Frame[]> frames);
function setFrames(frames: number[]): void;

Sets new frames to be played.

const Frame*  release  ();

Releases ownership of animation frames and returns it.

Changing Sprite To Animate

SpriteRef  sprite  () const;

Returns the target sprite.

void  Animation::set_sprite  (const SpriteRef &sprite);
function setSprite(sprite: Sprite): void;

Sets the sprite to animate.

Animation Callback Events

There are three events that are fired during an animation's lifetime.

You can subscribe to these events using:

void   Animation::set_callback  (Animation::Callback f);

// Where `Animation::Callback` is a callable whose signature is
// `void(Animation *animation, AnimationEvent event)`, and `animation` is the
// animation object that triggered `event`.
function setCallback(callback: (animation: Animation, event: AnimationEvent) => void): void;

// Example:

(animation, event) => {
  switch (event) {
    case Rainbow.AnimationEvent.Start:
      console.log("> Animation has started");
      break;
    case Rainbow.AnimationEvent.End:
      console.log("> Animation has ended");
      break;
    case Rainbow.AnimationEvent.Complete:
      console.log("> Animation has completed");
      break;
    case Rainbow.AnimationEvent.Frame:
      console.log("> Animation is displaying frame", animation.currentFrame());
      break;
  }
}

Example

In this example, we set up a walking animation.

#include "FileSystem/FileSystem.h"
#include "Graphics/Animation.h"
#include "Script/GameBase.h"

namespace
{
    constexpr int kTextureRegions[]{
        400, 724, 104, 149,
        504, 724, 104, 149,
        608, 724, 104, 149,
        712, 724, 104, 149,
        816, 724, 104, 149,
        920, 724, 104, 149};

    rainbow::Animation::Frame kAnimationFrames[]{
        0, 1, 2, 3, 4, 5, rainbow::Animation::Frame::end()};
}

auto load_texture()
{
    auto texture_path = rainbow::filesystem::relative("monkey.png");
    auto texture = rainbow::make_shared<rainbow::TextureAtlas>(texture_path.c_str());
    texture->set_regions(kTextureRegions);
    return texture;
}

void animation_event_handler(rainbow::Animation*, rainbow::AnimationEvent e)
{
    switch (e)
    {
        case rainbow::AnimationEvent::Start:
            // Handle animation start here.
            break;
        case rainbow::AnimationEvent::End:
            // Handle animation end here.
            break;
        case rainbow::AnimationEvent::Complete:
            // Handle animation cycle complete here.
            break;
        case rainbow::AnimationEvent::Frame:
            // Handle animation frame update here.
            break;
    }
}

class AnimationExample final : public rainbow::GameBase
{
public:
    AnimationExample(rainbow::Director& director)
        : rainbow::GameBase(director), batch_(1),
          animation_(rainbow::SpriteRef{},
                     rainbow::Animation::Frames(kAnimationFrames),
                     6,
                     0)
    {
    }

    ~AnimationExample() { animation_.release(); }

private:
    rainbow::SpriteBatch batch_;
    rainbow::Animation animation_;

    void init_impl(const rainbow::Vec2i& screen) override
    {
        rainbow::graphics::TextureManager::Get()->set_filter(
            rainbow::graphics::TextureFilter::Nearest);

        auto texture = load_texture();
        batch_.set_texture(texture);

        auto sprite = batch_.create_sprite(104, 149);
        sprite->set_position(rainbow::Vec2f(screen.x * 0.5f, screen.y * 0.5f));

        animation_.set_sprite(sprite);
        animation_.set_callback(&animation_event_handler);

        render_queue().emplace_back(batch_);
        render_queue().emplace_back(animation_);

        animation_.start();
    }
};

auto rainbow::GameBase::create(rainbow::Director& director)
    -> std::unique_ptr<rainbow::GameBase>
{
    return std::make_unique<AnimationExample>(director);
}
/// <reference path="./index.d.ts" />

interface World {
  screen: { width: number; height: number };
  batch: Rainbow.SpriteBatch;
  animation: Rainbow.Animation;
}

let world: World;

function init(width: number, height: number) {
  const texture = new Rainbow.Texture("monkey.png");
  const walkingFrames = [
    texture.addRegion(400, 724, 104, 149);
    texture.addRegion(504, 724, 104, 149);
    texture.addRegion(608, 724, 104, 149);
    texture.addRegion(712, 724, 104, 149);
    texture.addRegion(816, 724, 104, 149);
    texture.addRegion(920, 724, 104, 149);
  ];

  const batch = new Rainbow.SpriteBatch(1);
  batch.setTexture(texture);

  const sprite = batch.createSprite(104, 149);
  sprite.setPosition({ x: width * 0.5, y: height * 0.5 });

  const animation = new Rainbow.Animation(sprite, walkingFrames, 6, 0);
  animation.start();

  Rainbow.RenderQueue.add(batch);
  Rainbow.RenderQueue.add(animation);

  world = {
    screen: { width, height },
    batch,
    animation
  };
}

function update(dt: number) {}

Output:

Walking animation

Caveats and Known Limitations

Currently, an animation object takes ownership of the frames array and will attempt to delete it on destruction.

Math (C++ only)

Small collection of common mathematical functions and convenient algorithms. All of these functions are available under namespace rainbow unless otherwise stated.

Power of 2

unsigned int  ceil_pow2  (unsigned int i);

Rounds i up to the nearest power of 2.

unsigned int  floor_pow2  (unsigned int i);

Rounds i down to the nearest power of 2.

bool  is_pow2  (unsigned int i);

Returns whether integer i is a power of 2.

Trigonometry

template <typename T, typename = FloatingPoint<T>>
T  degrees  (T r);

template <typename T, typename = FloatingPoint<T>>
T  radian   (T d);

Converts values between degrees and radians.

Vec2

Vec2 is a two-dimensional vector template whose value type must be arithmetic. There are currently three predefined Vec2 types in the global namespace.

using Vec2f = Vec2<float>;
using Vec2i = Vec2<int>;
using Vec2u = Vec2<unsigned>;

Constants

template <typename T>
const Vec2<T>  Vec2<T>::Down;

Vector representing down; shorthand for Vec2<T>(0, -1).

template <typename T>
const Vec2<T>  Vec2<T>::Left;

Vector representing left; shorthand for Vec2<T>(-1, 0).

template <typename T>
const Vec2<T>  Vec2<T>::One;

One vector; shorthand for Vec2<T>(1, 1).

template <typename T>
const Vec2<T>  Vec2<T>::Right;

Vector representing right; shorthand for Vec2<T>(1, 0).

template <typename T>
const Vec2<T>  Vec2<T>::Up;

Vector representing up; shorthand for Vec2<T>(0, 1).

template <typename T>
const Vec2<T>  Vec2<T>::Zero;

Zero vector; shorthand for Vec2<T>(0, 0).

Methods

float  Vec2<T>::angle  (const Vec2 &v) const;

Returns the angle (in radians) between two points.

float  Vec2<T>::distance  (const Vec2 &v) const;

Returns the distance between two points.

float  Vec2<T>::distance_sq  (const Vec2 &v) const;

Returns the distance between two points, squared. This method skips the square root step performed in distance() and is therefore preferred unless you need the exact distance.

bool  Vec2<T>::is_zero()  const;

Returns whether both vector components are zero.

Operators

Assuming v and w are instances of Vec2f, Rainbow implements the following operators:

Vector Negation

Vec2f w = -v;

Equivalent to w = Vec2f(-v.x, -v.y).

Vector Sum

Vec2f u = v + w;
v += w;

Equivalent to u = Vec2f(v.x + w.x, v.y + w.y).

Vector Difference

Vec2f u = v - w;
v -= w;

Equivalent to u = Vec2f(v.x - w.x, v.y - w.y).

Dot Product

float f = v * w;

Equivalent to f = v.x * w.x + v.y * w.y.

Scalar Multiplication

Vec2f u = 2.0f * v;
v *= 2.0f;

Equivalent to u = Vec2f(2.0f * v.x, 2.0f * v.y).

Scalar Division

Vec2f u = v / 2.0f;
v /= 2.0f;

Equivalent to u = Vec2f(v.x / 2.0f, v.y / 2.0f).

Scalar Addition/Subtraction

Vec2f u = v + 5.0f;
Vec2f w = v - 5.0f;

Equivalent to u = Vec2f(v.x + 5.0f, v.y + 5.0f).

Comparison

bool equal = v == w;

Equivalent to v.x == w.x && v.y == w.y.

Miscellaneous

template <typename T, typename = FloatingPoint<T>>
bool  are_equal  (T a, T b);

Returns whether two floating point numbers are (almost) equal. Based on the AlmostEqualRelative() implementation from a Random ASCII blog post.

template <typename T>
T  clamp  (T x, T min_val, T max_val);

Returns the restricted value of x clamped in the range [min_val, max_val].

float  fast_invsqrt  (float x);

Returns an approximation of 1/√x using the infamous constant 0x5f3759df.

bool  is_almost_zero  (float x);

Returns whether x is practically zero. A number is considered almost zero if it's within 10 * ε. On some hardware, this is 0.0000011920929.

Timers (C++ only)

Timers perform delayed actions or repeat actions at set intervals.

Timer Management

Timers are managed by a global TimerManager. Any void returning callable can be used to create a timer. Timers may safely be created and cleared by timer callbacks. Additionally, timers may clear themselves on callback without causing error.

To access the timer manager, use the static Get() method:

TimerManager* timer_manager = TimerManager::Get();

Creating and Clearing Timers

Timer*  TimerManager::set_timer    (Timer::Closure func,
                                    const int interval,
                                    const int repeat_count);
  void  TimerManager::clear_timer  (Timer* t);

Timer::Closure is defined as a callable whose signature is void(). The interval is the time, in milliseconds, to wait before the first call and between any subsequent calls. A 0 interval will immediately set the timer in a finished state. Calls are repeated repeat_count times, e.g. a repeat count of 4 makes the timer perform an action 5 times. For infinity, use any negative value. TimerManager::set_timer() returns a Timer object which can be used to pause and resume the timer, as well as querying information.

Pausing and Resuming Timers

void  Timer::pause   ();
void  Timer::resume  ();

Pausing a timer will freeze it in its current state until it is resumed.

Timer Information

bool  Timer::is_active  () const;

Returns whether the timer is currently active. This method returns true while the timer is running normally, and false when it is finished or has been paused.

int  Timer::elapsed  () const;

Returns the elapsed time.

int  Timer::interval  () const;

Returns the timer's delay/interval in milliseconds.

int  Timer::repeat_count  () const;

Returns the number of times the action will be repeated. Note that this number excludes the first call.

Example

The following example creates four timers. Two that are performed 5 times (repeated 4 times) with an interval of 500 ms, and two that are performed after a 500 ms delay. We use a freestanding function, a functor, a lambda function, and a bound member function to create them. For simplicity's sake, we only clear the first repeated timer.

#include <functional>

#include "Script/GameBase.h"
#include "Script/Timer.h"

void action()
{
    LOGI("A freestanding function was called.");
}

class RepeatableAction
{
public:
    void action()
    {
        LOGI("A bound, repeated action was performed: %i time(s)", ++counter_);
    }

    void operator()()
    {
        LOGI("A repeated action was performed: %i time(s)", ++counter_);
    }

private:
    int counter_ = 0;
};

class TimerExample final : public rainbow::GameBase
{
public:
    TimerExample(rainbow::Director& director)
        : rainbow::GameBase(director), repeated_(nullptr)  {}

private:
    rainbow::Timer* repeated_;
    RepeatableAction action_;

    void init_impl(const rainbow::Vec2i&) override
    {
        // Create a delayed action using a function.
        rainbow::TimerManager::Get()->set_timer(&action, 500, 0);

        // Create a repeated action using a functor.
        repeated_ =
            rainbow::TimerManager::Get()->set_timer(RepeatableAction{}, 500, 4);

        // Create a delayed action using a lambda function.
        rainbow::TimerManager::Get()->set_timer(
            [] { LOGI("A delayed action was performed."); }, 500, 0);

        // Create a repeated action using a bound member function.
        rainbow::TimerManager::Get()->set_timer(
            std::bind(&RepeatableAction::action, action_), 500, 4);
    }

    void update_impl(uint64_t) override
    {
        if (repeated_ && !repeated_->is_active())
        {
            rainbow::TimerManager::Get()->clear_timer(repeated_);
            repeated_ = nullptr;
            LOGI("The repeated timer was cleared.");
        }
    }
};

auto rainbow::GameBase::create(rainbow::Director& director)
    -> std::unique_ptr<rainbow::GameBase>
{
    return std::make_unique<TimerExample>(director);
}

Output:

[1428273728390|INFO] A freestanding function was called.
[1428273728390|INFO] A repeated action was performed: 1 time(s)
[1428273728390|INFO] A delayed action was performed.
[1428273728390|INFO] A bound, repeated action was performed: 1 time(s)
[1428273728847|INFO] A repeated action was performed: 2 time(s)
[1428273728847|INFO] A bound, repeated action was performed: 2 time(s)
[1428273729352|INFO] A repeated action was performed: 3 time(s)
[1428273729352|INFO] A bound, repeated action was performed: 3 time(s)
[1428273729863|INFO] A repeated action was performed: 4 time(s)
[1428273729863|INFO] A bound, repeated action was performed: 4 time(s)
[1428273730380|INFO] A repeated action was performed: 5 time(s)
[1428273730380|INFO] A bound, repeated action was performed: 5 time(s)
[1428273730380|INFO] The repeated timer was cleared.

Caveats and Known Limitations

Timer handlers are reused. This implies that an old handler may be used to manipulate a more recent timer.

Transitions (C++ only)

Transitions provide a simple way to animate properties of a sprite such as opacity or position.

Functions

Rainbow implements a set of transitions for any objects that implement the appropriate methods. Duration is specified in milliseconds.

Definitions

Components must implement all listed methods for each requirement of a transition. Which definitions they must implement will be specified.

Colourable

A colourable component must implement methods for getting and setting colour.

Color  Colourable::color      () const;
void   Colourable::set_color  (Color);

Rotatable

A rotatable component must implement a method for rotating it relative to its current angle. The value passed is in radians.

void   Rotatable::rotate  (float);

Scalable

A scalable component must implement methods for getting and setting the component's current scale factors individually on each axis.

Vec2f  Scalable::scale      () const;
void   Scalable::set_scale  (Vec2f);

Translatable

A translatable component must implement a method for retrieving the component's current position, and one for moving it relatively to its current position.

Vec2f  Translatable::position  () const;
void   Translatable::move      (Vec2f);

Fade Transition

template <typename T>
rainbow::Timer*  rainbow::fade  (T component,
                                 int opacity,
                                 int duration,
                                 TimingFunction timing)
template <typename T>
rainbow::Timer*  rainbow::fade  (T component,
                                 float opacity,
                                 int duration,
                                 TimingFunction timing)

Useful for fading objects in/out, or for pulse-fading.

Valid opacity values are in the range of 0 to 255, inclusive, for the integer version, and 0.0 to 1.0 for the float version.

Components must be colourable.

Move Transition

template <typename T>
rainbow::Timer*  rainbow::move  (T component,
                                 Vec2f destination,
                                 int duration,
                                 TimingFunction timing)

Animates component moving towards destination.

Components must be translatable.

Rotate Transition

template <typename T>
rainbow::Timer*  rainbow::rotate  (T component,
                                   float angle,
                                   int duration,
                                   TimingFunction timing)

Animates component rotating towards angle radians.

Components must be rotatable.

Scale Transition

template <typename T>
rainbow::Timer*  rainbow::scale  (T component,
                                  float factor,
                                  int duration,
                                  TimingFunction timing)
template <typename T>
rainbow::Timer*  rainbow::scale  (T component,
                                  Vec2f factor,
                                  int duration,
                                  TimingFunction timing)

The first version scales the component uniformly on both axes. Use the second version when you want to specify the scale factor for each axis individually.

Components must be scalable.

Timing Functions

The following functions are available:

To see how each of these behave visually, see Easing Functions Cheat Sheet.

Example

#include "FileSystem/FileSystem.h"
#include "Script/GameBase.h"
#include "Script/Transition.h"

class TransitionExample final : public rainbow::GameBase
{
public:
    TransitionExample(rainbow::Director &director) : rainbow::GameBase(director)
    {
    }

private:
    rainbow::spritebatch_t batch_;

    void init_impl(const rainbow::Vec2i &screen) override
    {
        batch_ = rainbow::spritebatch(1);

        auto logo_path = rainbow::filesystem::relative("rainbow-logo.png");
        auto texture = rainbow::texture(logo_path);
        batch_->set_texture(texture);

        auto logo = batch_->create_sprite(392, 710);
        logo->set_color(rainbow::Color{0xffffff00});
        logo->set_position(rainbow::Vec2f{screen.x * 0.5f, screen.y * 0.5f});
        logo->set_scale(0.5);
        logo->set_texture(texture->add_region(1, 1, 392, 710));
        render_queue().emplace_back(batch_);

        rainbow::fade(logo, 1.0f, 1500, rainbow::timing::linear);
    }
};

auto rainbow::GameBase::create(rainbow::Director& director)
    -> std::unique_ptr<rainbow::GameBase>
{
    return std::make_unique<TransitionExample>(director);
}

Output (refresh if you missed the animation):

Fade-In Animation

Caveats and Known Limitations

Transitions are based on timers and will therefore run regardless of the enabled state of the component's render unit. The Timer object returned by the transition function can be used to pause/resume the animation.

Coding Standard

For starters, please read through the C++ Core Guidelines. Some of its points may only be repeated here on this page for emphasis.

General Practices

Interfaces

Namespaces

Comments

Use XML documentation. It is supported by both Doxygen and Visual Studio's IntelliSense.

Logging

Source Files

Headers

Filenames must end in .h, and should be self-contained. That means that you should be able to simply include it without having to include or declare anything else.

Header Guards

All header files must have #define guards. The format of the macro should be the full path of the file starting from src/. E.g. the file src/Common/impl/DataMap_Unix.h should have the following guard:

#ifndef COMMON_IMPL_DATAMAP_UNIX_H_
#define COMMON_IMPL_DATAMAP_UNIX_H_

#endif

Names and Order of Includes

  1. Corresponding header
  2. C/C++ library
  3. Third-party libraries
  4. Rainbow headers

Within each section, order the includes alphabetically. The paths to Rainbow's header files must always be descendants of src/. Don't use relative paths, or paths with . or ...

For example, see src/ThirdParty/Spine/spine-rainbow.cpp:

#include "ThirdParty/Spine/spine-rainbow.h"

#include <algorithm>

#include <spine/SkeletonJson.h>
#include <spine/extension.h>

#include "Common/DataMap.h"
#include "FileSystem/Path.h"
#include "Graphics/Renderer.h"
#include "Graphics/SpriteVertex.h"
#include "Graphics/TextureAtlas.h"

Forward Declarations

Use forward declarations whenever possible to avoid unnecessary #includes.

Classes

Declaration Order

Declare public members first, followed by protected, and finally private. Within each section, use the following order:

  1. usings, enums, and inner classes/structs
  2. Constants (static const data members)
  3. Static methods
  4. Data members (except static const data members)
  5. Constructors
  6. Destructor
  7. Accessors and mutators
  8. Methods
  9. Event handlers
  10. Operator overloads
  11. Overrides

Naming Conventions

Formatting

Use the provided .clang-format configuration to format any written code. There should be a ClangFormat plugin for your favourite editor:

Using Linters

You can tell CMake to run clang-tidy as part of the build process by passing -DCMAKE_CXX_CLANG_TIDY=/path/to/clang-tidy as a parameter to the build script.

Other Guidelines

License

Copyright © 2010-present Bifrost Entertainment AS and Tommy Nguyen

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.