Fruit Ninja

A Dragonfly Tutorial

Version 1.1

This is a tutorial to make an arcade style game akin to the classic Fruit Ninja game using the Dragonfly game engine.

The recommendation is to read the tutorial below and analyze the code provided. When prompted, download the next version of the code, compile and run it. Then, exxamine the code downloaded in relationship to the description provided. Feel free to modify the code, even, as you go to see what effects it has. When familiar with the code as presented, go on to the next step.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

0. Game On!

Download and setup the Dragonfly game engine, appropriate for your development environment.

Then, check out the first "game":

  //
  // game.cpp - Fruit Ninja
  // 

  // Engine includes.
  #include "GameManager.h"
  #include "LogManager.h"

  ///////////////////////////////////////////////
  int main(int argc, char *argv[]) {

    // Start up game manager.
    if (GM.startUp())  {
      LM.writeLog("Error starting game manager!");
      GM.shutDown();
      return 0;
    }

    // Setup logging.
    LM.setFlush(true);
    LM.setLogLevel(1);
    LM.writeLog("Fruit Ninja (v%.1f)", VERSION);

    // Dragonfly splash screen.
    df::splash();

    // Shut everything down.
    GM.shutDown();

    // All is well.
    return 0;
  }

Note the df:: tag in front of the Dragonfly-specific code elements. This is to access the Dragonfly namespace. Namespaces provide a mechanism to prevent potential name conflicts in large projects. Typically large, 3rd-party libraries (such as a game engine) will use a namespace to help developers avoid conflicts in names their own code may use with names the libraries use. In the case of Dragonfly, df:: is needed in game code to access any Dragonfly engine-specific code.

The #includes at the top of the program are the game engine header files needed so far. Basically, each header file is needed for each game engine service that is used (although some are included by the engine services themselves). Generally, in the source code files provided for download header includes are divided into three sections: a) those needed for system functionality (e.g., I/O), b) those needed for game engine functionality, and c) those used by the game code itself. Needed header includes are not shown (or discussed) for the rest of this tutorial.

The GameManager (and all Dragonfly game engine managers) are singletons, meaning there can be one and only one instance of each for each game. Managers cannot be created via a normal constructor (or assigned or copied), but must be accessed via a getInstance() call. After that, the Managers' methods can be accessed normally. For readability and convenience, the one (and only one) instance of each manager can be accessed by a two-letter acronym. Note, in the code above, GM is for the GameManager (manager of the game loop) and LM is for the LogManager (manager of the logfile).

Here, the GameManager starts up (via startUp()) and enables all other engine services. When the game is done, the game manager shuts down (via shutDown()), turning off all game engine services. All Dragonfly games do the same, with the startUp() first and the shutDown() last.

The LogManager is used by the engine to write to the log file, which is named "dragonfly.log" by default (Tip: the logfile name can be changed from the default in the df-config.txt configuration file, located where the Fruit Ninja source code is, and restarting the game). During debugging and development, game programs can write messages to the logfile via the LogManager, too. The LogManager supports C's printf() notation, providing flexibility in printing out the values of variables. In our code so far, if there is an error in starting up the game manager, this is reported in the log file and the game engine is shut down.

The line LM.setFlush(true) makes it so the LogManager flushes all output to local storage immediately after a writeLog() call. This is useful when developing as that way if a game crashes (e.g., a segfault), the logfile output is still written to local storage instead of being lost in memory. Generally, when a game is finished (e.g., ready for release) setFlush() is set to false (also the default) to avoid the overhead of writing imediately.

The df::splash() line invokes the Dragonfly splash screen, which includes zooming text and a dragonfly that turns into a 'y'. The splash screen is not strictly needed by any game, but including it here in the first game is useful to help ensure the development environment is setup and working properly.

Dragonfly games need to be compiled using the path for includes to the Dragonfly header files and the SFML header files. The Dragonfly library and the SFML libraries (graphics, window, system and audio) need to be linked in.

Once setup, games can be compiled via make (if using Linux or Mac OS command line compilation) or Build/F7 (if using Windows Visual Studio). Doing so will ensure all 3rd party libraries are installed (e.g., SFML and Dragonfly) and that the Makefile/Project file is setup correctly properly. Once built, games can be run from the shell via ./game (if in Linux or Mac) or via F5 from Visual Studio (if in Windows).

Checkpoint

Download and compile the game and try it out!

Game 0: fruit-ninja-0.zip

When run, the current game, such as it is, should pop-up a window, show the Dragonfly splash screen, then exit. A file named "dragonfly.log" should be created win the directory where the game was run, with some game engine messages. You can open and read the logfile with your favorite text editor.

Assets: Although this first version of the game does not need any sprites or sound effects, all sprites and sounds needed for the final version of the game can be downloaded at the link below. Note, they are also included with the zipped game versions as building progresses.

Sprites and Sounds: sprites-and-sounds.zip

Tip: If the game window that pops up is too small (or too big), the starting size can be conrtolled by the file df-config.txt located where the Fruit Ninja source code is. Open it with a text editor and change (or uncomment) lines with window_horizontal_pixels and window_vertical_pixels. Using a "0" for these values and setting window_style:fullscreen can work well for setups with a single monitor. The config file is only loaded when the engine starts, so after changing it, restart the game.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

1. Tutti-Fruity

Let's get some fruit ready for slicing! There is one Fruit class for all type of fruits. We'll create it, instantiate a Fruit and watch it fly across the screen.

1.1 Create Fruit

The different fruit types are identified by a std::string, with five types defined in game.h:

  // Fruit settings.
  const int NUM_FRUITS = 5;
  const std::string FRUIT[NUM_FRUITS] = {
    "pear",
    "grapes",
    "apple",
    "banana",
    "blueberries"
  };

The Fruit (and nearly all objects the game programmer uses to interact with the engine) is derived from the df::Object class.

  // Fruit.h

  class Fruit : public df::Object {

   private:
    bool m_first_out;

    // Handle out events.
    int out(const df::EventOut *p_e);

    // Handle collision events.
    int collide(const df::EventCollision *p_e);

  public:

    // Constructor - supply name of Fruit (matches Sprite).
    Fruit(std::string name);

    // Handle events.
    int eventHandler(const df::Event *p_e) override;

    // Setup starting conditions.
    void start(float speed);
  };

The Dragonfly Object (and derived classes) gets updates when things happen in the game, such as time passing, objects moving and colliding, and so on.

The Fruit constructor takes in the name of the Fruit and must match the label supplied when the Sprite is loaded (in this tutorial, the label matches the sprite filename -- read on for more information on Sprites).

  // Constructor - supply name of Fruit (matches Sprite).
  Fruit::Fruit(std::string name) {
    setType(name);
    if (setSprite(name) != 0)
      LM.writeLog("Fruit::Fruit(): Error! No sprite: %s", name.c_str());
    m_first_out = true;  // To ignore first out of bounds (when spawning).
    setSolidness(df::SOFT);
  }

Note, if the Sprite name is not found (e.g., it was not properly loaded by the Resource Manager), an error message is written to the Dragonfly logfile (dragonfly.log). The LM refers to the LogManager, a Dragonfly manager that facilitates writing messages (typically via writeLog()) to the logfile. The writeLog() method supports C-style printf() formatting.

Tip: When calling LM.writeLog(), it is often helpful to include from where the call is being made. A good convention to follow is to include the class name and the method name as part of the log message (e.g., Fruit::Fruit()). This is especially important when the game gets larger and runs for awhile there are lots of logfile messages written.

1.2 Dragonfly Sprites

The Dragonfly ResourceManager (RM), is used to load and manage the sprites (and sound effects and music - we'll do those later). A game object can associate with a loaded sprite by using the sprite label. Sprite files themselves are formatted as human-readable text. Take a look at the sprite file for an apple (apple.txt) below:

  <HEADER>
  frames 2
  width 5
  height 3
  color red
  slowdown 3
  </HEADER>
  <BODY>
   ,(. 
  (   )
   `"' 
  end
   ,). 
  (   )
   `"' 
  end
  </BODY>
  <FOOTER>
  version 1
  </FOOTER>

The sprite file is divided into three sections: HEADER, BODY and FOOTER. The top six lines in the HEADER provide information on the number of frames, frame width, frame height, sprite color, and animation slowdown, respectively. There is also a option (not shown) for a transparency field that specifies a character that will not be drawn (e.g., for a background). The subsequent BODY lines provide the frames to be animated (2 different frames, in the example above), each delimited by a single line with "end". The last FOOTER section has sprite version information. For this tutorial, you do not need to modify or add any sprite files, but can do so upon tutorial completion (e.g., to add a new kind of Fruit) using any text editor.

Sprites must be loaded from their native format into memory in a format usable by the Dragonfly game engine. All Sprites for the game are typically loaded before the game starts. To load a Sprite from a file, the RM::loadSprite() method is used. In Fruit Ninja, a function called from main() is used to load all resources after the game engine is started (i.e., after GM.startUp()) and before the game starts (i.e., before GM.run()).

  // Load resources (sprites, sound effects, music).
  void loadResources(void) {

    // Fruit sprites.
    for (int i=0; i<NUM_FRUITS; i++) {
      std::string name = "sprites/" + FRUIT[i] + ".txt";
      RM.loadSprite(name, FRUIT[i]);
    }
  }

1.3 Fruit Start

When spawning, each Fruit chooses a random starting location (the begin vector) just off the screen with velocity set to move to a random target location on the opposite side (the end vector):

  // Setup starting conditions.
  void Fruit::start(float speed) {

    df::Vector begin, end;

    // Get world boundaries.
    int world_x = (int) WM.getBoundary().getHorizontal();
    int world_y = (int) WM.getBoundary().getVertical();
    df::Vector world_center(world_x/2.0f, world_y/2.0f);

    // Pick random side (Top, Right, Bottom, Left) to spawn.
    switch (rand() % 4) {

    case 0: // Top.
      begin.setX((float) (rand()%world_x));
      begin.setY(0 - 3.0f);
      end.setX((float) (rand()%world_x));
      end.setY(world_y + 3.0f);
      break;

    case 1: // Right.
      ...
    }

    // Move Object into position.
    WM.moveObject(this, begin);

    // Set velocity towards opposite side.
    df::Vector velocity = end - begin;
    velocity.normalize();
    setDirection(velocity);
    setSpeed(speed);
  }

1.3 Fruit Event Handling

Once setup with a starting position and a speed and direction (i.e., a velocity), the Dragonfly engine will move a Fruit automatically each tick of the game loop (set to 30 Hz, by default). Game objects receive events as the game loop progresses and those events are delivered to game objects via the eventHandler() method. A Fruit is interested in "out of bounds" events and collision events. And other events are ignored.

  // Handle event.
  int Fruit::eventHandler(const df::Event *p_e) {

    // Out of bounds event.
    if (p_e -> getType() == df::OUT_EVENT)
      return out((df::EventOut *) p_e);

    // Collision event.
    if (p_e -> getType() == df::COLLISION_EVENT)
      return collide((df::EventCollision *) p_e);

    // Not handled.
    return 0;
  }

The out of bounds event (df::OUT_EVENT) occurs when a game object is inside the boundaries of the game world (for Fruit Ninja, this is the screen) and moves completely outside the game world (so, moves off the screen). When the Fruit has moved off the screen, this means the player has failed to slice it so the Fruit destroys itself - this last bit happens in WM.markForDelete(this) as a request to the game engine to delete the game object at the next world update. There is one exception - at the top is code to check if this is the first time the Fruit has moved out of bounds. This happens when the object first spawns and moves to the starting location , so is ignored. After that, an out of bounds event means what was said initially - that the object has moved off the screen without being sliced.

  // Handle out events.
  int Fruit::out(const df::EventOut *p_e) {

    if (m_first_out) { // Ignore first out (when spawning).
      m_first_out = false;
      return 1;
    }

    // Destroy this Fruit.
    WM.markForDelete(this);

    // Handled.
    return 1;
  }

The other game engine event that Fruit cares about is a collision event (df::COLLISION_EVENT), but only collisions with the player's slicer - the Sword (introduced later). When this happens, the fruit destroys itself (WM.markForDelete()) and creates an explosion in its destructor.

  // Handle collision events.
  int Fruit::collide(const df::EventCollision *p_e) {

    // Sword collision means ninja sliced this Fruit.
    if (p_e -> getObject1() -> getType() == SWORD_STRING) {

      // Destroy this Fruit.
      WM.markForDelete(this);
    }

    // Handled.
    return 1;
  }

1.4 Fruit Explosions

Fruit's are exploded in the destrucor by calling the Dragonfly df::explode() function to create particle effects that shatter a Sprite into individual characters.

  // Destructor.
  Fruit::~Fruit() {

    // If inside the game world and engine not shutting down,
    // create explosion and play sound.
    if (df::boxContainsPosition(WM.getBoundary(), getPosition()) &&
        GM.getGameOver() == false) {
      df::explode(getAnimation().getSprite(), getAnimation().getIndex(), getPosition(),
                  EXPLOSION_AGE, EXPLOSION_SPEED, EXPLOSION_ROTATE);
    }
  }

1.5 Flying Fruit.

The last step is to add code to main() to instantiate a Fruit and run.

  // Load resources.
  loadResources();

  // Spawn a Fruit.
  Fruit *p_f = new Fruit(FRUIT[rand() % NUM_FRUITS]);
  p_f -> start(0.25f);

  // Run game (this blocks until game loop is over).
  GM.run();

Checkpoint

Download the game and try it out!

Game 1: fruit-ninja-1.zip

You should see a Fruit that starts off the screen move slowly across the screen. You can try changing the Fruit speeds (the number passed into the Fruit::start() call), if you want, to see the effect it has (units are in spaces per game loop tick). That's about it at this point since there is no Sword (and no Points display). But you can try it a few times and see Fruits spawning and moving in action. Study the full code base, too. When ready, go to the next step.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

2. The Sword - the Perfect Fruit Slicer

The player interacts with the game by moving the mouse, which moves the virtual sword (a blue +) that leaves a trail. Moving through a fruit slices it. This interaction is done with the Sword class. Let's add a sword to the game to allow some interaction!

2.1 Setup

The Sword is df::SPECTRAL so it does not generate collisions inside the engine (it computes those itself in game code) and at the df::MAX_ALTITUDE so it is drawn on top of all other game objects.

The Sword is interested in mouse events (df::MSE_EVENT) and keyboard events (df::KEYBOARD_EVENT), which let it respond to user mouse and keyboard input, respectively. It indicates interest via the registerInterest() method calls. The Sword is also interested in step events (df::STEP_EVENTS), which are generated each tick of the game loop. Each step, the Sword checks for slicing and creates a movement trail. Lastly, the starting position is set (the center of the screen) and all other local attributes.

  // Constructor.
  Sword::Sword() {

    setType("Sword");
    setSolidness(df::SPECTRAL);
    setAltitude(df::MAX_ALTITUDE); // Make Sword in foreground.

    registerInterest(df::MSE_EVENT);
    registerInterest(df::KEYBOARD_EVENT);
    registerInterest(df::STEP_EVENT);

    // Start sword in center of world.
    df::Vector p(WM.getBoundary().getHorizontal()/2,
                 WM.getBoundary().getVertical()/2);
    setPosition(p);

    m_old_position = getPosition();
    m_color = df::CYAN;
    m_sliced = 0;
    m_old_sliced = 0;
  }

The Sword is not associated with a Sprite. Instead, each tick of the game loop, it draws itself on the screen via at its current (x,y) location (getPosition()) using the Dragonfly DisplayManager (DM) drawCh() method.

  // Draw sword on window.
  int Sword::draw() {
    return DM.drawCh(getPosition(), SWORD_CHAR, m_color);
  }

As noted in the constructor, the Sword is interested in 3 types of events: 1) mouse events, since it moves with the player's mouse, 2) step events which is uses to determine slicing and making a trail, and 3) keyboard events to let the player exit the game by pressing the q key. These are all passed by the game engine to the Sword via the eventHandler() method with the general df::Event being cast into the specific events as appropriate.

  // Handle event.
  int Sword::eventHandler(const df::Event *p_e) {

    // Mouse event.
    if (p_e->getType() == df::MSE_EVENT)
      return mouse((df::EventMouse *) p_e);

    // Step event.
    if (p_e->getType() == df::STEP_EVENT)
      return step((df::EventStep *) p_e);

    // Keyboard event.
    if (p_e->getType() == df::KEYBOARD_EVENT)
      return keyboard((df::EventKeyboard *) p_e);

    // If get here, have ignored this event.
    return 0;
  }

2.2 Movement

Upon a mouse event, the Sword simply updates its position (setPosition()) to be where the mouse is (getMousePosition()).

  // Handle mouse event.
  int Sword::mouse(const df::EventMouse *p_e) {

    // If "move", change position to mouse position.
    if (p_e -> getMouseAction() == df::MOVED) {
      setPosition(p_e -> getMousePosition());
      return 1;
    }

    // If get here, not handled.
    return 0;
  }

2.3 Slicing

Step event handling is a bit more involved. First, the Sword checks if it has moved positions since the last step. If it has not, it resets the m_sliced counter (which is used for combo bonuses, described later) and does nothing else. If it has moved, there are two main tasks: A) create a trail, and B) check for fruit slicing. Creating a trail is done with the create_trail() method, described later. Fruit slicing computes a line from the old position to the new one. Then, it goes through all the game objects in the world and for each object: 1) if it is a Fruit (determined via a dynamic_cast), and 2) the line intersects the Fruit (determined via lineIntersectsBox()), the Fruit is "sliced". Slicing is handled by creating a collision event (df::EventCollision) and sending it to the Fruit eventHandler() method (see Fruit::eventHanlder() above).

  // Handle step event.
  int Sword::step(const df::EventStep *p_e) {

    // Check if moved since last step.
    if (m_old_position == getPosition()) {
      m_sliced = 0;
      return 1;
    }

    // Make a trail from last position to current.
    create_trail(getPosition(), m_old_position);

    // Check if line intersects any Fruit objects.
    df::Line line(getPosition(), m_old_position);
    df::ObjectList ol = WM.solidObjects();

    for (int i=0; i<ol.getCount(); i++) {

      // Only slice Fruit.
      if (!(dynamic_cast <Fruit *> (ol[i])))
        continue;

      // If line from previous position intersects --> slice!
      df::Object *p_o = ol[i];
      df::Box box = getWorldBox(p_o);
      if (lineIntersectsBox(line, box)) {
        df::EventCollision c(this, p_o, p_o->getPosition());
        p_o -> eventHandler(&c);
        m_sliced += 1;

       } // End of box-line check.

    } // End of loop through all objects.

    m_old_position = getPosition();

    // Handled.
    return 1;
  }

2.4 Leaving a Trail

When the sword moves, it creates a visual trail made of up of small objects between the old position and the new one via the create_trail() function, below. Basically, the function computes a set of points from p1 to p2 and spawns a particle at each of them. A particle is a small, simple, visual element used to simulate complex effects by using many at once. In this case, the particle is a df::Fader that shines brightly but fades automatically over time.

  // Create trail from p1 to p2.
  void create_trail(df::Vector p1, df::Vector p2) {

    const float size = 2;
    const int age = 20;
    const int opacity = 255;
    const df::Color color = df::CYAN;
    unsigned char r, g, b;
    df::colorToRGB(color, r, g, b);

    // Calculate step size for interpolation.
    float dist = df::distance(p1, p2) * 10;
    float dX = (p1.getX() - p2.getX()) / (dist + 1.0f);
    float dY = (p1.getY() - p2.getY()) / (dist + 1.0f);

    // Create Fader particles on line from p1 to p2.
    for (int i=0; i<dist; i++) {
      float x = p2.getX() + dX*i;
      float y = p2.getY() + dY*i;
      df::Fader *p_f = new df::Fader(size, age, opacity, r, g, b);
      p_f -> setPosition(df::Vector(x,y));
    }
  }

2.5 Keyboard Input

The keyboard event is handled just so the player can exit the game by pressing Q. Right now, that is done by setting "game over" to true in the GameManager (i.e., GM::setGameOver()). We'll modify this exiting behavior later so as to end the game a little less abruptly, but it works for now.

  // Handle keyboard event.
  int Sword::keyboard(const df::EventKeyboard *p_e) {

    if (p_e->getKey() == df::Keyboard::Q &&
       p_e->getKeyboardAction() == df::KEY_PRESSED) {
      GM.setGameOver(true);
      return 1;
    }

    // If get here, not handled.
    return 0;
  }

2.6 Populate World

Similarly to the load_resources() function, it can be cleaner to have one place to populate the game world with all initial objects before the game engine is started. We can do that in populateWorld() in util.cpp:

  // Populate the world with game objects.
  void populateWorld(void) {
    new Sword();
  }

Checkpoint

Download the game and try it out!

Game 2: fruit-ninja-2.zip

You should see a Sword that zips around the screen with movements of the mouse, leaving a trail. When it intersects the Fruit, the Fruit should explode. Since you can only slice one Fruit per game, you'll need to exit and restart the game a few times to see and slice the full set of Fruits. Study the full code base, too, to understand how it works as a set. When ready, go to the next step.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

3. The Grocer - Keepin' it Fresh

We now have a single Fruit and a fruit-slicer (the Sword), so are ready to add more Fruit. That is done via a Grocer game object.

3.1 Setup

The Grocer does not interact with the game world as do other game objects, but instead just monitors time (virtual time, via the step event ticks) and spawns Fruits accordingly. The Grocer constructor makes it df::SPECTRAL`` (so it does not generate nor receive collisions) and invisible (so is not drawn), and interested in step events (df::STEP_EVENT`). Other parameters control the game difficulties, via waves.

  Grocer::Grocer(){
    setType(GROCER_STRING);
    setSolidness(df::SPECTRAL);
    setVisible(false);
    registerInterest(df::STEP_EVENT);
    m_wave = 1;
    m_wave_end = WAVE_LEN;
    m_wave_speed = WAVE_SPEED; // Starting speed (spaces/tick).
    m_wave_spawn = WAVE_SPAWN; // Starting spawn rate (ticks).
    m_spawn = m_wave_spawn;
  }

3.2 Time

The eventHandler() is set up as for other game objects, calling step() when there is a step event. Each tick, the spawn countdown (m_spawn) is decremented. When it reaches 0, a Fruit is spawned. The variety of Fruit is controlled by using a modulo and the wave number (i.e., more types of Fruit spawn later in the game as waves progress). The start speed is set via Fruit::start() and increases with wave.

Also each tick, the wave countdown (m_wave_end) is decremented. When less than 0, the wave is advanced (m_wave) and Fruit speeds (via m_wave_speed) and spawn rates (via m_wave_spawn) increase. When the last wave is reached (NUM_WAVES), the game is over. The Grocer::gameOver() method (not shown), just sets GM.setGameOver(true).

  // Handle step event.
  int Grocer::step(const df::EventStep *p_e) {

    // Fruit grocer.
    m_spawn -= 1;
    if (m_spawn < 0) {

      int mod = m_wave+1 > NUM_FRUITS ? NUM_FRUITS : m_wave + 1;
      Fruit *p_f = new Fruit(FRUIT[rand() % mod]);
      if (!p_f) {
        LM.writeLog("Grocer::step(): Error! Unable to allocate Fruit.");
        return 0;
      }

      p_f -> start(m_wave_speed);

      m_spawn = m_wave_spawn;
    }

    // Advance wave.
    m_wave_end -= 1;
    if (m_wave_end < 0) {

      m_wave_end = WAVE_LEN;
      m_wave_spawn += SPAWN_INC; // Increase spawn rate.
      m_wave_speed += SPEED_INC; // Increase Fruit speed.
      m_wave += 1;
      if (m_wave == NUM_WAVES+1)
        gameOver();
    }

    return 1;
  }

Checkpoint

Game 3: fruit-ninja-3.zip

Download the game and try it out! You should be able to slice Fruit for about a minute, with different types spawning automatically. Fruit variety and Fruit speed should increase with time until the game ends. Play a few rounds. Modify some of the Grocer parameters if you like to get a feel for how it changes difficulty. When you have a good feel for the game and code, proceed to the next step.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

4. What's the Point?

Time to give get to the point of the game - literally - by adding a Points object. The Points object is derived not from an df::Object but from a df::ViewObject:

  // Points.h
  class Points : public df::ViewObject {

   public:

    // Constructor.
    Points();

    // Set value.
    void setValue(int value) override;
  };

Dragonfly ViewObjects are special types of df::Objects that stay in the same position on the screen and do not interact (collide) with other game objects. The Points object displays "Points" in a white box in the top right of the screen, with the starting value 0.

  Points::Points() {
    setType(POINTS_STRING);
    setLocation(df::TOP_RIGHT);
    setViewString(POINTS_STRING);
    setColor(df::WHITE);
    setValue(0);
  }

The only other logic used for Fruit Ninja is to not allow Points to go negative (which can happen if the player fails to slice a bunch of Fruit before they fly off the screen).

  void Points::setValue(int value) {

    // Call parent.
    ViewObject::setValue(value);

    // If less than 0, set to 0.
    if (getValue() < 0)
      ViewObject::setValue(0);
  }

Once created, there are a few places to add code to give the player points. The first is in Fruit collide() method. If the Sword has collided with the Fruit, then the player gets +10 points, delivered to the Points object by a Dragonfly event (a df::EventView).

  // Add points.
  df::EventView ev(POINTS_STRING, +10, true);
  WM.onEvent(&ev);

The true boolean tells the Points object to add the 10 points to the current value. If it was false, Points would set the value to 10. The Fruit also penalizes the player -25 when they have missed (i.e., the Fruit has gone off the screen without being sliced) in the out() method:

  // Each out is a "miss", so lose points.
  df::EventView ev(POINTS_STRING, -25, true);
  WM.onEvent(&ev);

The last place for Points modification (for now) is in the Sword. In order to discourage the player from "spamming" sword slicing back and forth, there is a slight Points penalty based on distance traveled:

  // Lose points for distance traveled.
  float dist = df::distance(getPosition(), m_old_position);
  int penalty = -1 * (int)(dist/10.0f);
  df::EventView ev("Points", penalty, true);
  WM.onEvent(&ev);

Checkpoint

Download the game and try it out!

Game 4: fruit-ninja-4.zip

The gameplay will be as before, but now the player can earn points for slicing (and lose points for missing). Feel free to change the point values (positive and negative) to get a feel for the gameplay. When comfortable with the code (and slicing and dicing), move on to the next step.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

5. Silent as a Ninja?

As they say, for a game, sound is 1/3 the experience, so, let's add some experience, er ... sounds.

Sounds are handled in Dragonfly similar to Sprites. First, all sounds need to be loaded into the engine, converting them from their original format on disk into a format usable by the engine. For Dragonfly, loading is typically done after the game engine starts but before the game is run -- ror Fruit Ninja, sounds are loaded in the loadResources() function (the same place where the Fruit sprites were loaded). Sounds include a variety of "splats" for the Fruit explosions and "swipes" for the Ninja sword.

  // Sounds.
  RM.loadSound("sounds/game-start.wav", "game-start");
  RM.loadSound("sounds/game-over.wav", "game-over");
  RM.loadSound("sounds/impact.wav", "impact");
  RM.loadSound("sounds/beep.wav", "beep");
  
  for (int i=1; i<=NUM_SPLATS; i++) {
    std::string sound = "splat-" + std::to_string(i);
    std::string file = "sounds/" + sound + ".wav";
    RM.loadSound(file, sound);
  }

  for (int i=1; i<=NUM_SWIPES; i++) {
    std::string sound = "swipe-" + std::to_string(i);
    std::string file = "sounds/" + sound + ".wav";
    RM.loadSound(file, sound);
  }

To play a sound, the associated sound effect is retrieved from the ResourceManager via the getSound() call. This returns a pointer (or NULL if the sound was not found). Fruit Ninja makes a utility (in util.cpp) to make it more convenient to play the indicates sound:

  // Play indicated sound.
  void play_sound(std::string sound) {
    df::Sound *p_sound = RM.getSound(sound);
    if (p_sound)
      p_sound->play();
    else
      LM.writeLog("play_sound(): Unable to get sound '%s'",
                  sound.c_str());
  }

With the sounds loaded and our play_sound() utility function, we only need to add sound effects to the code in he right spots. First, when a Fruit explodes, it should play a "splat" sound effect in the Fruit destructor:

  // Play "splat" sound.
  std::string sound = "splat-" + std::to_string(rand()%6 + 1);
  play_sound(sound);

And in the Sword step() method, if the player made a mighty swing of the blade (i.e., a large enough travel distance -- 15 spaces), it should play a "swipe" sound effect:

  // If travel far enough, play "swipe" sound.
  float dist = df::distance(getPosition(), m_old_position);
  if (dist > 15) {
    std::string sound = "swipe-" + std::to_string(rand()%7 + 1);
    play_sound(sound);
  }

Checkpoint

Download the game and try it out!

Game 5: fruit-ninja-5.zip

The gameplay will be as before, but the player should have sound effects to accompany sword-swinging and Fruit slicing. You can play around with the "swipe" distance if you want and even try more (or fewer) sound effeects and see what that does to the game. When familiar with the code and what it does, go on to the next step.


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

6. Bells and Whistles

We have a full-featured, fully playable game, but without some niceties.

6.1 Timer

First, it would be good for the player to know how much time is left in the game. We can do that with a Timer -- a ViewObject (like Points above), but one that counts down the seconds in its step() method. Basically, every 30 ticks (about 1 second), Timer decrements the starting value (set to 50 in the constructor). The color starts white, but turns yellow at 20 seconds and red at 10 seconds. When the timer is less than 12 seconds, it plays a "beep" every other second and when less than 6 seconds plays a "beep" every second.


  // Handle step events.
  int Timer::step(const df::EventStep *p_e) {

    // Countdown the seconds.
    if (p_e -> getStepCount() % 30 == 0 && getValue() > 0)
      setValue(getValue() - 1);
    else
      return 1;

    // Time running out - yellow.
    if (getValue() <= 20 && getValue() > 10)
      setColor(df::YELLOW);

    // Time running out - red.
    if (getValue() < 10)
      setColor(df::RED);

    // Sound warning as time expires.
    if (getValue() < 13 && getValue() % 2 == 0 ||
        getValue() < 6 && getValue() > 0)
      play_sound("beep");

    // Handled.
    return 1;
  }

6.2 Game Over

Instead of having the game close abruptly when time is up, we can fade out gracefully. This is done with a GameOver game object. The GameOver object sets the time to live (m_time_to_live) to last as long as the animation (which diplays the letter G-A-M-E O-V-E-R one at a time). It also shakes the screen for a visual indicator (DM for the DisplayManager and shake() for the shake method).

  GameOver::GameOver() {
    setType(GAMEOVER_STRING);
    setDrawValue(false);

    // Animate "game over" sprite one time.
    if (setSprite("gameover") == 0)
      m_time_to_live = getAnimation().getSprite()->getFrameCount() *
                       getAnimation().getSprite()->getSlowdown();
    else
      m_time_to_live = 0;

    // Put in center of window.
    setLocation(df::CENTER_CENTER);

    // Register for step event.
    registerInterest(df::STEP_EVENT);

    // Shake screen (severity 20 pixels x&y, duration 10 frames).
    DM.shake(20, 20, 10);

    LM.writeLog("GameOver::GameOver(): created");
  }
  // Count down to end of G-A-M-E O-V-E-R message.
  int GameOver::step() {

    m_time_to_live--;

    if (m_time_to_live <= 0) {
      GM.setGameOver(true);
      WM.markForDelete(this);
    }

    if (m_time_to_live == 175)
      play_sound("game-over");

    // Handled.
    return 1;
  }

Then, change the gameOver() method in the Grocer to spawn the GameOver game object and have it destroy all Fruit:

  // Do game over actions.
  void Grocer::gameOver() {
    new GameOver();

    // Destroy all remaining Fruit (no points).
    df::ObjectList ol = WM.solidObjects();
    for (int i=0; i<ol.getCount(); i++)
      if (dynamic_cast <Fruit *> (ol[i]))
        WM.markForDelete(ol[i]);

    WM.markForDelete(this);
  }

With GameOver functionality in place, when time runs out, any Fruit on the screen explodes and the words Game Over appear on the screen before the game exits.

6.3 Kudos

The skilled Ninja can slice several fruit in a single swipe of the blade and so should be rewarded. When this happens, Fruit Ninja responds with a "way to go!" message delivered by a Kudos class. The Kudos class uses a Sprite that has a bunch of individual affirmations and picks one at random, placing it on the screen in a random spot for 30 ticks (1 second). A sound effect comes with it, with the sound sequence building with more Kudos. Lastly, the Kudos delivers a bonus +50 points to the player.

  Kudos::Kudos() {
    setType(KUDOS_STRING);
    setSolidness(df::SPECTRAL);
    setAltitude(df::MAX_ALTITUDE);
    setSprite("kudos");

    // Pick random kudos to show.
    df::Animation a = getAnimation();
    const df::Sprite *p_sprite = getAnimation().getSprite();
    if (p_sprite) {
      int count = p_sprite -> getFrameCount();
      int kudos = rand() % count;
      a.setIndex(kudos);
    }
    a.setSlowdownCount(-1); // Not animated.
    setAnimation(a);

    // Pick random location.
    df::Vector p(WM.getBoundary().getHorizontal()/8 +
                 rand() % (3 * (int) WM.getBoundary().getHorizontal()/4),
                 WM.getBoundary().getVertical()/8 +
                 rand() % (3 * (int) WM.getBoundary().getVertical()/4));
    setPosition(p);

    // Stays on the screen for 1 second.
    m_time_to_live = 30; 
    registerInterest(df::STEP_EVENT);

    // Play next Kudos sound.
    static int s_sound = 1;  // next kudos sound
    std::string sound = "kudos-" + std::to_string(s_sound);
    play_sound(sound);
    s_sound += 1;
    if (s_sound > 10)
      s_sound = 10;

    // Extra points.
    df::EventView ev(POINTS_STRING, 50, true);
    WM.onEvent(&ev);
  }

Next, the trigger for a Kudos needs to be added to the Sword slicing, in the Sword::step() method. Basically, if the player slices 3 Fruit in a row, that deserves a Kudos.

  // Spawn kudos for combo.
  if (m_sliced > 2 && m_sliced > m_old_sliced)
    new Kudos();

6.4 Fruit Ninja Splash

A total bell (or whistle) is a game splash screen, customized for the game. In Fruit Ninja, this is done via a Splash game object. Basically, in the step() method, it runs through a scripted series of animations (show below with comments, not code):

// Handle step events.
int Splash::step(const df::EventStep *p_s) {

  // Time 1: Spawn Fruit. Play sound.
  if (m_time == FRUIT_TIME) {
    // New Fruit splash.
    // Plus fruit.
    // Play "impact" sound.
  }

  // Time 2: Spawn Ninja. Play sound.
  if (m_time == NINJA_TIME) {
    // New Ninja splash.
    // Plus fruit.
    // Play "impact" sound.
  }

  // Time 3: Slice and explode.
  if (m_time == SLICE_TIME) {
    // Slice.
    // Explode fruit-splash.
    // Explode ninja-splash.
    // Explode Fruit.
    // Play "game start" sound.
  }

  // Time 4: Done.
  if (m_time >= END_TIME) {
    // Delete everything.
    // Set game over.
  }

  // Advance time.
  m_time += 1;
}

In order to instantiate the new splash, a new Splash() is added after the Dragonfly splash is created in main() but still before GM.run().

Final Checkpoint

Download the final version of the tutorial game and try it out!

Game 6: fruit-ninja-6.zip

This is the last version of Fruit Ninja, ready for full-featured, all the bells and whistles, slicing-and-dicing. But read on for some ideas on how to extend the game!


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

7. Fruit Salad

Hopefully, you have been able to follow the tutorial for a good understanding of how the code works and why. You may already have a bunch of ideas on how the final Fruit Ninja game can be extended. In case more ideas are needed, here are a few ...

Bombs Away: Instead of a Fruit, sometimes a bomb can fly across the screen as a dangerous decoy. Bombs should not be sliced by the Ninja and doing so means losing points (and maybe some screen shaking and explosion effects). You'll probably want a new Bomb class for this, similar to a Fruit in many ways but different behavior when sliced. Also, you'll want to make a new sprite. For a twist, you could make the bomb color random to match different Fruit colors for more deception.

Move It, Move It: Different types of movement could be added to the Fruit, beyond just constant velocity across the screen. One mode could have Fruit thrown from the bottom of the screen as if tossed up, and have gravity (a constant acceleration down) applied to the objects. The initial velocity and gravity would need to be tweaked to get it to "feel right". Other interesting movement could have the Fruit move in a zig-zag pattern or even juking in response to the sword, getting trickier as the waves advance.

Do the Wave: Currently, the waves handled by the Grocer simply add more Fruit that move slightly faster. A richer set of waves could be built in, with controlled Fruit launching and behaviors. Each wave could start slowly and build to a climax then having a pause, perhaps accompanied with a message to the player (e.g., "End of Wave 1"). The Grocer could probably handle all of this, but you could consider making a Wave class and instantiate different versions of the Wave class as the game progresses.

Make a Point: The player could have access to new swords as the game progresses. They could have different colors (e.g., purple) and trail effects. They could also have different capabilities, perhaps with a wider blade or trail that can collide with moving Fruit. New swords could be earned (say, as levels are completed) or purchased (say, by spending points). This latter feature would need a new system for purchasing, but the df::Button class could be helpful here.

Pop Art: While there is some slicing and splatting variety, there is plenty of room for additional audio art. More sound effects could be good, perhaps to cover other game events or Fruit types. Similarly, there is plenty of room for additional Fruit, both types, colors and even sizes. These could be incorporated as the game progresses (e.g., small fast Fruit appear later) or with new features (e.g., new Swords).


Top | Game On | Tutti-Fruity | Sword | Grocer | Points | Sound | Extra | Next

Have fun!

-- Mark Claypool