SimpleBall Source Code
Jump to navigation
Jump to search
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);
}