Version 1.1
This is a tutorial to make an arcade style game akin to the classic
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.
Download and setup the
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()) {
.writeLog("Error starting game manager!");
LM.shutDown();
GMreturn 0;
}
// Setup logging.
.setFlush(true);
LM.setLogLevel(1);
LM.writeLog("Fruit Ninja (v%.1f)", VERSION);
LM
// Dragonfly splash screen.
::splash();
df
// Shut everything down.
.shutDown();
GM
// All is well.
return 0;
}
Note the df::
tag in front of the
df::
is
needed in game code to access any
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 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 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 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
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 ./game
(if in Linux or Mac) or
via F5 from Visual Studio (if in Windows).
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
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 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.
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.
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).
(std::string name);
Fruit
// Handle events.
int eventHandler(const df::Event *p_e) override;
// Setup starting conditions.
void start(float speed);
};
The
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(std::string name) {
Fruit(name);
setTypeif (setSprite(name) != 0)
.writeLog("Fruit::Fruit(): Error! No sprite: %s", name.c_str());
LMm_first_out = true; // To ignore first out of bounds (when spawning).
(df::SOFT);
setSolidness}
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.log
). The
LM
refers to the LogManager, a 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.
The 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 RM::loadSprite()
method is used. In 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";
.loadSprite(name, FRUIT[i]);
RM}
}
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) {
::Vector begin, end;
df
// Get world boundaries.
int world_x = (int) WM.getBoundary().getHorizontal();
int world_y = (int) WM.getBoundary().getVertical();
::Vector world_center(world_x/2.0f, world_y/2.0f);
df
// Pick random side (Top, Right, Bottom, Left) to spawn.
switch (rand() % 4) {
case 0: // Top.
.setX((float) (rand()%world_x));
begin.setY(0 - 3.0f);
begin.setX((float) (rand()%world_x));
end.setY(world_y + 3.0f);
endbreak;
case 1: // Right.
...
}
// Move Object into position.
.moveObject(this, begin);
WM
// Set velocity towards opposite side.
::Vector velocity = end - begin;
df.normalize();
velocity(velocity);
setDirection(speed);
setSpeed}
Once setup with a starting position and a speed and direction (i.e.,
a velocity), 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.
.markForDelete(this);
WM
// 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.
.markForDelete(this);
WM}
// Handled.
return 1;
}
Fruit's are exploded in the destrucor by calling the 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()) &&
.getGameOver() == false) {
GM::explode(getAnimation().getSprite(), getAnimation().getIndex(), getPosition(),
df, EXPLOSION_SPEED, EXPLOSION_ROTATE);
EXPLOSION_AGE}
}
The last step is to add code to main()
to instantiate a
Fruit and run.
// Load resources.
();
loadResources
// Spawn a Fruit.
*p_f = new Fruit(FRUIT[rand() % NUM_FRUITS]);
Fruit -> start(0.25f);
p_f
// Run game (this blocks until game loop is over).
.run(); GM
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.
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!
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
("Sword");
setType(df::SPECTRAL);
setSolidness(df::MAX_ALTITUDE); // Make Sword in foreground.
setAltitude
(df::MSE_EVENT);
registerInterest(df::KEYBOARD_EVENT);
registerInterest(df::STEP_EVENT);
registerInterest
// Start sword in center of world.
::Vector p(WM.getBoundary().getHorizontal()/2,
df.getBoundary().getVertical()/2);
WM(p);
setPosition
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 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;
}
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) {
(p_e -> getMousePosition());
setPositionreturn 1;
}
// If get here, not handled.
return 0;
}
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.
(getPosition(), m_old_position);
create_trail
// Check if line intersects any Fruit objects.
::Line line(getPosition(), m_old_position);
df::ObjectList ol = WM.solidObjects();
df
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!
::Object *p_o = ol[i];
df::Box box = getWorldBox(p_o);
dfif (lineIntersectsBox(line, box)) {
::EventCollision c(this, p_o, p_o->getPosition());
df-> eventHandler(&c);
p_o m_sliced += 1;
} // End of box-line check.
} // End of loop through all objects.
m_old_position = getPosition();
// Handled.
return 1;
}
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;
::colorToRGB(color, r, g, b);
df
// 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;
::Fader *p_f = new df::Fader(size, age, opacity, r, g, b);
df-> setPosition(df::Vector(x,y));
p_f }
}
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 &&
->getKeyboardAction() == df::KEY_PRESSED) {
p_e.setGameOver(true);
GMreturn 1;
}
// If get here, not handled.
return 0;
}
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();
}
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.
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.
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(GROCER_STRING);
setType(df::SPECTRAL);
setSolidness(false);
setVisible(df::STEP_EVENT);
registerInterestm_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;
}
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;
*p_f = new Fruit(FRUIT[rand() % mod]);
Fruit if (!p_f) {
.writeLog("Grocer::step(): Error! Unable to allocate Fruit.");
LMreturn 0;
}
-> start(m_wave_speed);
p_f
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;
}
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.
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;
};
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(POINTS_STRING);
setType(df::TOP_RIGHT);
setLocation(POINTS_STRING);
setViewString(df::WHITE);
setColor(0);
setValue}
The only other logic used for
void Points::setValue(int value) {
// Call parent.
::setValue(value);
ViewObject
// If less than 0, set to 0.
if (getValue() < 0)
::setValue(0);
ViewObject}
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 df::EventView
).
// Add points.
::EventView ev(POINTS_STRING, +10, true);
df.onEvent(&ev); WM
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.
::EventView ev(POINTS_STRING, -25, true);
df.onEvent(&ev); WM
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);
::EventView ev("Points", penalty, true);
df.onEvent(&ev); WM
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.
As they say, for a game, sound is 1/3 the experience, so, let's add some experience, er ... sounds.
Sounds are handled in 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.
.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");
RM
for (int i=1; i<=NUM_SPLATS; i++) {
std::string sound = "splat-" + std::to_string(i);
std::string file = "sounds/" + sound + ".wav";
.loadSound(file, sound);
RM}
for (int i=1; i<=NUM_SWIPES; i++) {
std::string sound = "swipe-" + std::to_string(i);
std::string file = "sounds/" + sound + ".wav";
.loadSound(file, sound);
RM}
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). util.cpp
) to make it more
convenient to play the indicates sound:
// Play indicated sound.
void play_sound(std::string sound) {
::Sound *p_sound = RM.getSound(sound);
dfif (p_sound)
->play();
p_soundelse
.writeLog("play_sound(): Unable to get sound '%s'",
LM.c_str());
sound}
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);
(sound); play_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);
(sound);
play_sound}
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.
We have a full-featured, fully playable game, but without some niceties.
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)
(getValue() - 1);
setValueelse
return 1;
// Time running out - yellow.
if (getValue() <= 20 && getValue() > 10)
(df::YELLOW);
setColor
// Time running out - red.
if (getValue() < 10)
(df::RED);
setColor
// Sound warning as time expires.
if (getValue() < 13 && getValue() % 2 == 0 ||
() < 6 && getValue() > 0)
getValue("beep");
play_sound
// Handled.
return 1;
}
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(GAMEOVER_STRING);
setType(false);
setDrawValue
// Animate "game over" sprite one time.
if (setSprite("gameover") == 0)
m_time_to_live = getAnimation().getSprite()->getFrameCount() *
().getSprite()->getSlowdown();
getAnimationelse
m_time_to_live = 0;
// Put in center of window.
(df::CENTER_CENTER);
setLocation
// Register for step event.
(df::STEP_EVENT);
registerInterest
// Shake screen (severity 20 pixels x&y, duration 10 frames).
.shake(20, 20, 10);
DM
.writeLog("GameOver::GameOver(): created");
LM}
// 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) {
.setGameOver(true);
GM.markForDelete(this);
WM}
if (m_time_to_live == 175)
("game-over");
play_sound
// 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).
::ObjectList ol = WM.solidObjects();
dffor (int i=0; i<ol.getCount(); i++)
if (dynamic_cast <Fruit *> (ol[i]))
.markForDelete(ol[i]);
WM
.markForDelete(this);
WM}
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.
The skilled Ninja can slice several fruit in a single swipe of the
blade and so should be rewarded. When this happens,
::Kudos() {
Kudos(KUDOS_STRING);
setType(df::SPECTRAL);
setSolidness(df::MAX_ALTITUDE);
setAltitude("kudos");
setSprite
// Pick random kudos to show.
::Animation a = getAnimation();
dfconst df::Sprite *p_sprite = getAnimation().getSprite();
if (p_sprite) {
int count = p_sprite -> getFrameCount();
int kudos = rand() % count;
.setIndex(kudos);
a}
.setSlowdownCount(-1); // Not animated.
a(a);
setAnimation
// Pick random location.
::Vector p(WM.getBoundary().getHorizontal()/8 +
df() % (3 * (int) WM.getBoundary().getHorizontal()/4),
rand.getBoundary().getVertical()/8 +
WM() % (3 * (int) WM.getBoundary().getVertical()/4));
rand(p);
setPosition
// Stays on the screen for 1 second.
m_time_to_live = 30;
(df::STEP_EVENT);
registerInterest
// Play next Kudos sound.
static int s_sound = 1; // next kudos sound
std::string sound = "kudos-" + std::to_string(s_sound);
(sound);
play_sounds_sound += 1;
if (s_sound > 10)
s_sound = 10;
// Extra points.
::EventView ev(POINTS_STRING, 50, true);
df.onEvent(&ev);
WM}
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();
A total bell (or whistle) is a game splash screen, customized for the
game. In 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 main()
but still before GM.run()
.
Download the final version of the tutorial game and try it out!
Game 6: fruit-ninja-6.zip
This is the last version of
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
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).
Have fun!
-- Mark Claypool