SimpleBall Source Code
This page contains the complete source code for the SimpleBall
example game module. See the Simple Games page for more information about the basic examples.
SimpleBall.h
#ifndef SimpleBall_h
#define SimpleBall_h
#include "C4Application.h"
#include "C4World.h"
#include "C4Input.h"
#include "C4Cameras.h"
#include "C4Interface.h"
#include "C4Particles.h"
// Every application/game module needs to declare a function called CreateApplication()
// exactly as follows. (It must be declared extern "C", and it must include the tag
// C4_MODULE_EXPORT.) The engine looks for this function in the DLL and calls
// it to create an instance of the subclass of the Application class that the
// application/game module defines.
extern "C"
{
C4_MODULE_EXPORT C4::Application *CreateApplication(void);
}
namespace SimpleBall
{
using namespace C4;
// These are action types used to define action bindings in the
// Input Manager. If the four-character code for an action is
// 'abcd', then any input control (there can be more than one)
// bound to %abcd triggers the associated action.
enum : ActionType
{
kActionForward = 'frwd',
kActionBackward = 'bkwd',
kActionLeft = 'left',
kActionRight = 'rght',
kActionUp = 'jump',
kActionDown = 'down'
};
// Model types are associated with a model resource using the ModelRegistration
// class. Models are registered with the engine in the Game constructor.
enum : ModelType
{
kModelBall = 'ball'
};
// New controller types are registered with the engine in the Game constructor.
enum : ControllerType
{
kControllerBall = 'ball'
};
// This is the type of our custom particle system.
enum : ParticleSystemType
{
kParticleSystemSpark = 'sprk'
};
// New locator types are registered with the engine in the Game constructor.
// The 'spec' locator is used to specify where the spectator camera should
// be positioned when a world is loaded.
enum : LocatorType
{
kLocatorSpectator = 'spec'
};
// An Action object represents an input action that can be triggered by some
// input control, such as a key on the keyboard or a button on a gamepad.
// The HandleEngage() and HandleDisengage() methods are called when the button
// is pressed and released, respectively. Actions are registered with the Input
// Manager when the Game class is constructed.
class MovementAction : public Action
{
private:
uint32 movementFlag;
public:
MovementAction(ActionType type, uint32 flag);
~MovementAction();
void HandleEngage(void) override;
void HandleDisengage(void) override;
};
// Controllers are used to control anything that moves in the world.
// New types of controllers defined by the application/game module are
// registered with the engine when the Game class is constructed.
//
// The BallController inherits from the built-in rigid body controller,
// which handles the ball's motion and collision detection. We are only
// adding a little bit of functionality that causes a particle system
// to be created when a ball hits another ball.
class BallController final : public RigidBodyController
{
private:
BallController(const BallController& ballController);
Controller *Replicate(void) const override;
public:
BallController();
~BallController();
static bool ValidNode(const Node *node);
void PreprocessController(void) override;
RigidBodyStatus HandleNewRigidBodyContact(const RigidBodyContact *contact, RigidBodyController *contactBody) override;
};
// The SparkParticleSystem class implements a simple particle system that
// creates a small burst of sparks.
class SparkParticleSystem : public LineParticleSystem
{
// This friend declaration allows the particle system registration object
// to construct a SparkParticleSystem object using the private default constructor.
friend class ParticleSystemReg<SparkParticleSystem>;
private:
enum {kMaxParticleCount = 100};
int32 sparkCount;
// This is where information about each particle is stored.
ParticlePool<> particlePool;
Particle particleArray[kMaxParticleCount];
SparkParticleSystem();
bool CalculateBoundingSphere(BoundingSphere *sphere) const override;
public:
SparkParticleSystem(int32 count);
~SparkParticleSystem();
void PreprocessNode(void) override;
void AnimateParticles(void) override;
};
// The StartWindow class is a simple example of a window that handles button clicks.
// We add the Global base class so that a pointer to the window can be tracked easily.
class StartWindow : public C4::Window, public Global<StartWindow>
{
private:
WidgetObserver<StartWindow> startButtonObserver;
WidgetObserver<StartWindow> quitButtonObserver;
void HandleStartButtonEvent(Widget *widget, const WidgetEventData *eventData);
void HandleQuitButtonEvent(Widget *widget, const WidgetEventData *eventData);
public:
StartWindow();
~StartWindow();
void PreprocessWidget(void) override;
bool HandleKeyboardEvent(const KeyboardEventData *eventData) override;
};
// The application/game module will usually define a subclass of the World
// class so that extra information can be associated with the current world.
// In this case, an instance of the SpectatorCamera class is included
// with the world. A new instance of this World subclass should be returned
// when the Game::CreateWorld() function is called (see below).
class GameWorld : public World
{
private:
SpectatorCamera spectatorCamera;
static const LocatorMarker *FindSpectatorLocator(const Zone *zone);
public:
GameWorld(const char *name);
~GameWorld();
SpectatorCamera *GetSpectatorCamera(void)
{
return (&spectatorCamera);
}
WorldResult PreprocessWorld(void) override;
void RenderWorld(void) override;
};
// Every application/game module needs to define a subclass of the Application
// class to serve as the primary interface with the engine. This subclass is
// created and returned to the engine in the CreateApplication() function.
// There should be only one instance of this class, and a pointer to it is
// declared below.
class Game : public Application, public Global<Game>
{
private:
ModelRegistration ballModelReg;
ControllerReg<BallController> controllerReg;
ParticleSystemReg<SparkParticleSystem> sparkParticleSystemReg;
LocatorRegistration locatorReg;
InputMgr::KeyCallback *prevEscapeCallback;
void *prevEscapeCookie;
MovementAction *forwardAction;
MovementAction *backwardAction;
MovementAction *leftAction;
MovementAction *rightAction;
MovementAction *upAction;
MovementAction *downAction;
static World *CreateWorld(const char *name, void *cookie);
static void EscapeCallback(void *cookie);
public:
Game();
~Game();
};
// This is a pointer to the one instance of the Game class through which
// any other part of the application/game module can access it.
extern Game *TheGame;
// This is a pointer to the start window. We only keep this around so that
// we can delete the window before exiting if it's still on the screen.
extern StartWindow *TheStartWindow;
}
#endif
SimpleBall.cpp
#include "SimpleBall.h"
using namespace SimpleBall;
// This is the definition of the pointer to the Game class global.
// It should be initialized to nullptr, and its value will be set by
// the Game class constructor.
Game *SimpleBall::TheGame = nullptr;
// This is the definition of the pointer to the StartWindow class global.
// It should be initialized to nullptr, and its value will be set by
// the StartWindow class constructor.
StartWindow *SimpleBall::TheStartWindow = nullptr;
C4::Application *CreateApplication(void)
{
// This function should simply return a pointer to a new instance of
// the Application class. Normally, the application/game module will
// define a subclass of the Application class (in this case, the
// Game class) and return a pointer to a new instance of that type.
// This function is called exactly one time right after the
// application/game module DLL is loaded by the engine. The returned
// class is destroyed via the virtual destructor of the Application
// class right before the application/game module DLL is unloaded.
return (new Game);
}
MovementAction::MovementAction(ActionType type, uint32 flag) : Action(type)
{
// Each instance of the MovementAction class represents a movement
// in a single direction, as indicated by the flag parameter.
// All of the MovementAction instances are constructed in the
// Game class constructor.
movementFlag = flag;
}
MovementAction::~MovementAction()
{
}
void MovementAction::HandleEngage(void)
{
// This function is called when the input control associated with this
// particular action is engaged (e.g., a key was pressed).
World *world = TheWorldMgr->GetWorld();
if (world)
{
// If there's currently a world loaded, apply the movement to
// the spectator camera.
SpectatorCamera *camera = static_cast<GameWorld *>(world)->GetSpectatorCamera();
camera->SetSpectatorFlags(camera->GetSpectatorFlags() | movementFlag);
}
}
void MovementAction::HandleDisengage(void)
{
// This function is called when the input control associated with this
// particular action is disengaged (e.g., a key was released).
World *world = TheWorldMgr->GetWorld();
if (world)
{
// If there's currently a world loaded, remove the movement from
// the spectator camera.
SpectatorCamera *camera = static_cast<GameWorld *>(world)->GetSpectatorCamera();
camera->SetSpectatorFlags(camera->GetSpectatorFlags() & ~movementFlag);
}
}
BallController::BallController() : RigidBodyController(kControllerBall)
{
// This constructor is called when a new ball model is created.
}
BallController::BallController(const BallController& ballController) : RigidBodyController(ballController)
{
// This constructor is called when a ball controller is cloned.
}
BallController::~BallController()
{
}
Controller *BallController::Replicate(void) const
{
return (new BallController(*this));
}
bool BallController::ValidNode(const Node *node)
{
// This function is called by the engine to determine whether this
// specific type of controller can be assigned to the particular
// node passed in through the node parameter. This function should
// return true if it can control the node, and otherwise it should
// return false. In this case, the controller can only be applied
// to model nodes.
return (node->GetNodeType() == kNodeModel);
}
void BallController::PreprocessController(void)
{
// This function is called once before the target node is ever
// rendered or moved. The base class PreprocessController() function
// should always be called first, and then the subclass can do whatever
// preprocessing it needs to do. In this case, we set a few of the
// ball's physical parameters and give it a random initial velocity.
RigidBodyController::PreprocessController();
SetRestitutionCoefficient(0.99F);
SetSpinFrictionMultiplier(0.1F);
SetRollingResistance(0.01F);
SetLinearVelocity(Random::RandomUnitVector3D() * 2.0F);
}
RigidBodyStatus BallController::HandleNewRigidBodyContact(const RigidBodyContact *contact, RigidBodyController *contactBody)
{
// This function is called when the ball makes contact with another rigid body.
// If we hit another ball, then we add a sound effect and some sparks to the world.
if (contactBody->GetControllerType() == kControllerBall)
{
Node *node = GetTargetNode();
World *world = node->GetWorld();
Point3D position = node->GetWorldTransform() * contact->GetContactPosition();
OmniSource *source = new OmniSource("model/Ball", 40.0F);
source->SetNodePosition(position);
world->AddNewNode(source);
SparkParticleSystem *sparks = new SparkParticleSystem(20);
sparks->SetNodePosition(position);
world->AddNewNode(sparks);
}
return (kRigidBodyUnchanged);
}
SparkParticleSystem::SparkParticleSystem(int32 count) :
// Initialize the base class for line particles
// and tell it where the particle pool is.
LineParticleSystem(kParticleSystemSpark, &particlePool, "particle/Spark1"),
// Initialize the particle pool by telling it where the array
// of Particle structs is and how big it is.
particlePool(kMaxParticleCount, particleArray)
{
// The SparkParticleSystem node creates a small burst of sparks
// and then self-destructs when all of them have burned out.
sparkCount = count;
SetLengthMultiplier(4.0F);
SetParticleSystemFlags(kParticleSystemSelfDestruct);
}
SparkParticleSystem::SparkParticleSystem() :
LineParticleSystem(kParticleSystemSpark, &particlePool, "particle/Spark1"),
particlePool(kMaxParticleCount, particleArray)
{
// This constructor gets used when the particle system is being loaded from
// a saved game. In this case, we don't need to initialize anything.
}
SparkParticleSystem::~SparkParticleSystem()
{
}
bool SparkParticleSystem::CalculateBoundingSphere(BoundingSphere *sphere) const
{
// Just return a sphere that's big enough to always enclose
// all of the particles. This is in object-space coordinates.
sphere->SetCenter(0.0F, 0.0F, 0.0F);
sphere->SetRadius(20.0F);
return (true);
}
void SparkParticleSystem::PreprocessNode(void)
{
// This function creates the spark particles.
// Always call the base class PreprocessNode() function.
LineParticleSystem::PreprocessNode();
// If there's already a particle in the system, then the node was loaded from
// a saved game. We only create new particles if the particle system is empty.
if (!GetFirstParticle())
{
// Calculate the world-space center.
Point3D center = GetSuperNode()->GetWorldTransform() * GetNodePosition();
const Vector2D *trig = Math::GetTrigTable();
int32 count = sparkCount;
for (machine a = 0; a < count; a++)
{
// Grab a new unused particle from the pool.
Particle *particle = particlePool.NewParticle();
if (!particle)
{
break;
}
particle->emitTime = 0; // Particle appears right away.
particle->lifeTime = 500 + Random::RandomInteger(750); // Particle lives 500-1250 milliseconds.
particle->radius = 0.02F; // The radius is 20 mm.
particle->color.Set(1.0F, 1.0F, 0.1F, 1.0F); // It's yellow.
particle->orientation = 0; // This doesn't matter for line particles.
particle->position = center; // It starts at the effect's center.
// Calculate a random velocity in a random direction.
float speed = Random::RandomFloat(0.004F);
const Vector2D& csp = trig[Random::RandomInteger(128)];
const Vector2D& cst = trig[Random::RandomInteger(256)];
particle->velocity.Set(cst.x * csp.y * speed, cst.y * csp.y * speed, csp.x * speed);
// Add the particle to the particle system.
AddParticle(particle);
}
}
}
void SparkParticleSystem::AnimateParticles(void)
{
// This function is called once per frame to move the particles.
// Times are in milliseconds.
int32 dt = TheTimeMgr->GetDeltaTime();
float fdt = TheTimeMgr->GetFloatDeltaTime();
Particle *particle = GetFirstParticle();
while (particle)
{
// Get the next particle here in case the current one is removed from the system.
Particle *next = particle->nextParticle;
int32 life = (particle->lifeTime -= dt);
if (life > 0)
{
// Update velocity with gravity.
particle->velocity.z += Const::gravity * fdt;
// Move the particle and see if it hit the floor plane at z = 0.
float z1 = particle->position.z - particle->radius;
particle->position += particle->velocity * fdt;
float z2 = particle->position.z - particle->radius;
if (z1 * z2 <= 0.0F)
{
// The particle hit the floor, so reflect its velocity and remove some energy.
particle->position.z = 0.05F - z2;
particle->velocity.z *= -0.5F;
}
// If the particle is nearing the end of its life, fade it out.
if (life < 100)
{
particle->color.alpha = (float) life * 0.01F;
}
}
else
{
// Particle burned out.
FreeParticle(particle);
}
particle = next;
}
}
// The StartWindow constructor initializes the Window base class with the name of the panel
// resource to load. The observer members are initialized with the functions that are
// called when the "Start" and "Quit" push buttons in the window post activate events.
StartWindow::StartWindow() :
Window("panel/SimpleBall"),
Global<StartWindow>(TheStartWindow),
startButtonObserver(this, &StartWindow::HandleStartButtonEvent),
quitButtonObserver(this, &StartWindow::HandleQuitButtonEvent)
{
}
StartWindow::~StartWindow()
{
}
void StartWindow::PreprocessWidget(void)
{
// We must call the Window base class PreprocessWidget() function first to initialize
// the internal structures that are used to search for widgets.
Window::PreprocessWidget();
// Assign observers to the push button widgets named "Start" and "Quit".
Widget *button = FindWidget("Start");
button->SetObserver(&startButtonObserver);
button = FindWidget("Quit");
button->SetObserver(&quitButtonObserver);
}
bool StartWindow::HandleKeyboardEvent(const KeyboardEventData *eventData)
{
if (eventData->eventType == kEventKeyDown)
{
if (eventData->keyCode == kKeyCodeEscape)
{
// The Escape key was pressed while this window had the focus.
// If a world is loaded, then just delete the window and return to gameplay.
if (TheWorldMgr->GetWorld())
{
delete this;
return (true);
}
}
}
return (false);
}
void StartWindow::HandleStartButtonEvent(Widget *widget, const WidgetEventData *eventData)
{
// This function is called whenever the "Start" push button posts an event.
if (eventData->eventType == kEventWidgetActivate)
{
// If the widget was activated, then the user clicked the push button.
// Delete the window and load a world to play.
delete this;
TheWorldMgr->LoadWorld("world/SimpleBall");
}
}
void StartWindow::HandleQuitButtonEvent(Widget *widget, const WidgetEventData *eventData)
{
// This function is called whenever the "Quit" push button posts an event.
if (eventData->eventType == kEventWidgetActivate)
{
// If the widget was activated, then the user clicked the push button.
// Quit the engine.
TheEngine->Quit();
}
}
GameWorld::GameWorld(const char *name) :
World(name),
spectatorCamera(2.67F, 1.0F, 0.3F)
{
// This constructor is called when the Game::CreateWorld() function is
// called to create a new world class. The world hasn't actually been loaded
// from disk yet when we get here.
}
GameWorld::~GameWorld()
{
}
const LocatorMarker *GameWorld::FindSpectatorLocator(const Zone *zone)
{
// Iterate through all of the markers in the zone.
const Marker *marker = zone->GetFirstMarker();
while (marker)
{
if (marker->NodeEnabled())
{
MarkerType type = marker->GetMarkerType();
if (type == kMarkerLocator)
{
const LocatorMarker *locator = static_cast<const LocatorMarker *>(marker);
if (locator->GetLocatorType() == kLocatorSpectator)
{
return (locator);
}
}
}
// Get the next marker in the list.
marker = marker->GetNextListElement();
}
// Recursively look in all of the subzones.
const Zone *subzone = zone->GetFirstSubzone();
while (subzone)
{
const LocatorMarker *locator = FindSpectatorLocator(subzone);
if (locator)
{
return (locator);
}
subzone = subzone->ListElement<Zone>::GetNextListElement();
}
return (nullptr);
}
WorldResult GameWorld::PreprocessWorld(void)
{
// The PreprocessWorld() function is called after the world has been constructed.
// We must always call the base class PreprocessWorld() function first. If it
// returns an error, then we just return the same result code.
WorldResult result = World::PreprocessWorld();
if (result != kWorldOkay)
{
return (result);
}
// The world is now completely loaded. We search for a locator node that represents the
// spectator camera's starting position. It has a locator type of kLocatorSpectator.
const LocatorMarker *marker = FindSpectatorLocator(GetRootNode());
if (marker)
{
// A spectator marker was found.
// Set the spectator camera's initial position and orientation.
const Vector3D direction = marker->GetWorldTransform()[0];
float azimuth = Arctan(direction.y, direction.x);
float altitude = Arctan(direction.z, Sqrt(direction.x * direction.x + direction.y * direction.y));
spectatorCamera.SetCameraAzimuth(azimuth);
spectatorCamera.SetCameraAltitude(altitude);
spectatorCamera.SetNodePosition(marker->GetWorldPosition());
}
else
{
// A spectator marker was not found.
// Put the spectator camera one meter above the origin by default.
spectatorCamera.SetNodePosition(Point3D(0.0F, 0.0F, 1.0F));
}
// Set the world's current camera to be our spectator camera.
// The world will not render without a camera being set.
SetWorldCamera(&spectatorCamera);
return (kWorldOkay);
}
void GameWorld::RenderWorld(void)
{
// This function is called once per frame to render the world.
// The subclass may do whatever it needs to before or after rendering,
// but at some point must call World::RenderWorld().
World::RenderWorld();
}
Game::Game() :
// This is the constructor for the main application/game module class.
// This class is created by the CreateApplication() function, which is
// called right after the application/game DLL is loaded by the engine.
// We initialize the global pointer to the current game instance first.
Global<Game>(TheGame),
// A model registration represents a model that can be instanced.
// This particular declaration associates the kModelBall type with the
// model named "model/Ball.mdl". The fourth parameter tells the engine
// to precache the model resource, and the last parameter specifies
// the default controller type to assign to models of type kModelBall.
ballModelReg(kModelBall, "Bouncing Ball", "model/Ball", kModelPrecache, kControllerBall),
// A controller registration tells the engine about an application-defined type of
// controller and registers its name. In this case, the name "Bouncing Ball" will
// appear in the list of available controllers in the World Editor for any node that
// is determined to be a valid target node by the BallController::ValidNode() function.
controllerReg(kControllerBall, "Bouncing Ball"),
// A particle system registration tells the engine about an application-defined
// type of particle system and registers its name.
sparkParticleSystemReg(kParticleSystemSpark, "Sparks"),
// Locator markers are registered so that the World Editor
// can display their names in the Markers page.
locatorReg(kLocatorSpectator, "Spectator Camera")
{
// This sets the function that is called when the user hits the
// escape key during gameplay. We save the old function so that
// it can be restored when the game DLL is unloaded.
prevEscapeCallback = TheInputMgr->GetEscapeCallback();
prevEscapeCookie = TheInputMgr->GetEscapeCookie();
TheInputMgr->SetEscapeCallback(&EscapeCallback, this);
// This registers our world class constructor with the World Manager.
// We only need to do this if we have defined a subclass of the World
// class that holds extra information.
TheWorldMgr->SetWorldCreator(&CreateWorld);
// These create the movement actions that are used to fly the spectator camera around.
forwardAction = new MovementAction(kActionForward, kSpectatorMoveForward);
backwardAction = new MovementAction(kActionBackward, kSpectatorMoveBackward);
leftAction = new MovementAction(kActionLeft, kSpectatorMoveLeft);
rightAction = new MovementAction(kActionRight, kSpectatorMoveRight);
upAction = new MovementAction(kActionUp, kSpectatorMoveUp);
downAction = new MovementAction(kActionDown, kSpectatorMoveDown);
// These register our new actions with the Input Manager.
TheInputMgr->AddAction(forwardAction);
TheInputMgr->AddAction(backwardAction);
TheInputMgr->AddAction(leftAction);
TheInputMgr->AddAction(rightAction);
TheInputMgr->AddAction(upAction);
TheInputMgr->AddAction(downAction);
// Let the Interface Manager determine when to change input devices to gameplay mode.
TheInterfaceMgr->SetInputManagementMode(kInputManagementAutomatic);
// Put the Message Manager in single-player mode.
TheMessageMgr->BeginSinglePlayerGame();
// Create the start window and tell the Interface Manager to display it.
TheInterfaceMgr->AddWidget(new StartWindow);
}
Game::~Game()
{
// When the game DLL is about to be unloaded, this destructor is called.
TheWorldMgr->UnloadWorld();
TheWorldMgr->SetWorldCreator(nullptr);
// If the start window exists, delete it. We need to do this here, instead of letting the
// Interface Manager clean it up, because the destructor code for the window is in this DLL.
delete TheStartWindow;
// Tell the Message Manager to clean up.
TheMessageMgr->EndGame();
delete downAction;
delete upAction;
delete rightAction;
delete leftAction;
delete backwardAction;
delete forwardAction;
// Restore the previous escape key handling function.
TheInputMgr->SetEscapeCallback(prevEscapeCallback, prevEscapeCookie);
}
World *Game::CreateWorld(const char *name, void *cookie)
{
// This function is called when a new world is being loaded. It should
// return a pointer to a newly constructed subclass of the World class.
return (new GameWorld(name));
}
void Game::EscapeCallback(void *cookie)
{
// This function is called when the user hits the escape key in gameplay
// mode because we registered it using the InputMgr::SetEscapeCallback() function.
// In this case, we open the start window shwoing the main menu.
TheInterfaceMgr->AddWidget(new StartWindow);
}