SimpleBall
From C4 Engine Wiki
The SimpleBall project is one of the very small game modules that ship with the C4 Engine to serve as a simple example of how to use various fundamental concepts. The two files composing the SimpleBall project are heavily commented and represent nearly the minimum amount of code that needs to be written to have a working game module.You can tell the engine to load this game module by editing the file Data/Engine/variables.cfg and changing the value of the variable $applicName to "SimpleBall". To get the primary game module back, change $applicName to "Game" again.
When the SimpleBall module is running, the world called “SimpleBall” can be loaded to show a demonstration of the collision detection system. (To load the world, type load world/SimpleBall in the Command Console.) The “SimpleBall” world contains 112 green bouncing balls in a small room, and every ball can collide with the environment or any other ball. The motion of the balls is completely controlled by the code in the SimpleBall application.
If the balls don't move it is probably because the world was opened and then saved while the the demo game module was being run. This causes the ball controller defined by the SimpleBall module to be removed because it's not defined. The controllers can only be restored by re-extracting the SimpleBall.wld file from the Data.zip file.
Below is the complete source code for the SimpleBall sample project.
Header File, SimpleBall.h
#ifndef SimpleBall_h #define SimpleBall_h #include "C4Application.h" #include "C4Input.h" #include "C4World.h" #include "C4Particles.h" // Every application/game module needs to declare a function called ConstructApplication() // exactly as follows. (It must be declared extern "C", and it must include the tag C4MODULEEXPORT.) // The engine looks for this function in the DLL and calls it to construct an instance of // the subclass of the Application class that the application/game module defines. extern "C" { C4MODULEEXPORT C4::Application *ConstructApplication(void); } 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 { 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 { kModelBall = 'ball' }; // New controller types are registered with the engine in the Game constructor. enum { kControllerBall = 'ball' }; // This is the type of our custom particle system. enum { 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 { 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 joystick. // The Begin() and End() 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: unsigned long movementFlag; public: MovementAction(unsigned long type, unsigned long flag); ~MovementAction(); void Begin(void); void End(void); }; // 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 : public RigidBodyController { public: BallController(); ~BallController(); static bool ValidNode(const Node *node); void Preprocess(void); RigidBodyStatus HandleNewRigidBodyContact(const RigidBodyContact *contact, RigidBodyController *contactBody); }; // The SparkSystem class implements a simple particle system that // creates a small burst of sparks. class SparkSystem : public ParticleSystem { // This friend declaration allows the particle system registration object // to construct a SparkSystem object using the private default constructor. friend class ParticleSystemReg<SparkSystem>; private: enum { kMaxParticleCount = 100 }; long sparkCount; // This is where information about each particle is stored. ParticlePool<> particlePool; Particle particleArray[kMaxParticleCount]; SparkSystem(); bool CalculateBoundingSphere(BoundingSphere *sphere) const; public: SparkSystem(long count); ~SparkSystem(); void Preprocess(void); bool AnimateParticles(void); }; // 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::ConstructWorld() function is called (see below). class GameWorld : public World { private: SpectatorCamera spectatorCamera; public: GameWorld(const char *name); ~GameWorld(); SpectatorCamera *GetSpectatorCamera(void) { return (&spectatorCamera); } WorldResult Preprocess(void); void Render(void); }; // 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 // constructed and returned to the engine in the ConstructApplication() function. // There should be only one instance of this class, so it inherits from the // Singleton template. A pointer to the Game instance is declared below. class Game : public Application, public Singleton<Game> { private: DisplayEventHandler displayEventHandler; ModelRegistration ballModelReg; ControllerReg<BallController> controllerReg; ParticleSystemReg<SparkSystem> sparkSystemReg; LocatorRegistration locatorReg; InputMgr::KeyProc *prevEscapeProc; void *prevEscapeData; MovementAction *forwardAction; MovementAction *backwardAction; MovementAction *leftAction; MovementAction *rightAction; MovementAction *upAction; MovementAction *downAction; static World *ConstructWorld(const char *name, void *data); static void HandleDisplayEvent(EventType eventType, long param, void *data); static void EscapeProc(void *data); 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; } #endif
Source File, SimpleBall.cpp
#include "SimpleBall.h" using namespace C4; // This is the definition of the pointer to the Game class singleton. // It should be initialized to nullptr, and its value will be set by // the Game class constructor. Game *C4::TheGame = nullptr; C4::Application *ConstructApplication(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(unsigned long type, unsigned long 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::Begin(void) { // This function is called when the input control associated with this // particular action is activated (e.g., a key was pressed). GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld()); if (world) { // If there's currently a world loaded, apply the movement to // the spectator camera. SpectatorCamera *camera = world->GetSpectatorCamera(); camera->SetSpectatorFlags(camera->GetSpectatorFlags() | movementFlag); } } void MovementAction::End(void) { // This function is called when the input control associated with this // particular action is deactivated (e.g., a key was released). GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld()); if (world) { // If there's currently a world loaded, remove the movement from // the spectator camera. SpectatorCamera *camera = world->GetSpectatorCamera(); camera->SetSpectatorFlags(camera->GetSpectatorFlags() & ~movementFlag); } } BallController::BallController() : RigidBodyController(kControllerBall) { // This constructor is only called when a new ball model is created. } BallController::~BallController() { } bool BallController::ValidNode(const Node *node) { // This function is called by the engine to determine whether // this particular type of controller can control 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::Preprocess(void) { // This function is called once before the target node is ever // rendered or moved. The base class Preprocess() function should // always be called first, and then the subclass can do whatever // preprocessing it needs to do. In this case, we don't do anything // extra, so this function is only overridden for illustrative purposes. RigidBodyController::Preprocess(); } RigidBodyStatus BallController::HandleNewRigidBodyContact(const RigidBodyContact *contact, RigidBodyController *contactBody) { // This function is called when the ball makes contact with another rigid body. if (contactBody->GetControllerType() == kControllerBall) { // Add a sound effect and some sparks to the zone containing the ball. // Transform the contact point into the zone's local coordinate system. Node *node = GetTargetNode(); Zone *zone = node->GetOwningZone(); Point3D zonePosition = zone->GetInverseWorldTransform() * contact->GetContactPosition(); OmniSource *source = new OmniSource("model/Ball", 50.0F); source->SetNodePosition(zonePosition); zone->AddNewSubnode(source); SparkSystem *sparks = new SparkSystem(20); sparks->SetNodePosition(zonePosition); zone->AddNewSubnode(sparks); } return (kRigidBodyUnchanged); } SparkSystem::SparkSystem(long count) : // Initialize the base class, tell it where the particle pool is, // and tell it what kind of particle we're drawing (lines). ParticleSystem(kParticleSystemSpark, &particlePool, kParticleLine), // Initialize the particle pool by telling it where the array of // Particle structs is and how big it is. particlePool(kMaxParticleCount, particleArray) { // The SparkEffect node creates a small burst of sparks and // then self-destructs when all of them have burned out. sparkCount = count; SetParticleSystemFlags(kParticleSystemSelfDestruct); } SparkSystem::SparkSystem() : // Initialize the base class, tell it where the particle pool is, // and tell it what kind of particle we're drawing (lines). ParticleSystem(kParticleSystemSpark, &particlePool, kParticleLine), // Initialize the particle pool by telling it where the array of // Particle structs is and how big it is. particlePool(kMaxParticleCount, particleArray) { // This constructor gets used when the particle effect is being loaded from // a saved game. In this case, we don't need to initialize anything. } SparkSystem::~SparkSystem() { } bool SparkSystem::CalculateBoundingSphere(BoundingSphere *sphere) const { // Just return a sphere that's big enough to always enclose // all of the particles. This is in local coordinates. sphere->SetCenter(0.0F, 0.0F, 0.0F); sphere->SetRadius(20.0F); return (true); } void SparkSystem::Preprocess(void) { // This function creates the spark particles. // Always call the base class Preprocess() function. ParticleSystem::Preprocess(); // If there's already a particle in the system, then the effect was loaded // from a saved game. We only create new particles if the system is empty. if (!GetFirstParticle()) { // Calculate the world-space center. Point3D center = GetSuperNode()->GetWorldTransform() * GetNodePosition(); const float *sine = Math::GetSineTable(); const float *cosine = Math::GetCosineTable(); long count = sparkCount; for (natural a = 0; a < count; a++) { // Grab a new unused particle from the pool. Particle *p = particlePool.NewParticle(); if (!p) break; p->emitTime = 0; // Particle appears right away. p->lifeTime = 500 + Math::Random(750); // Particle lives 500-1250 milliseconds. p->radius = 0.025F; // The radius is 25 mm. p->color.Set(1.0F, 1.0F, 0.1F, 1.0F); // It's yellow. p->orientation = 0; // This doesn't matter for line particles. p->position = center; // It starts at the effect's center. // Calculate a random velocity in a random direction. long phi = Math::Random(128); long theta = Math::Random(256); float speed = Math::RandomFloat(0.004F); float s = sine[phi] * speed; float c = cosine[phi] * speed; p->velocity.Set(cosine[theta] * s, sine[theta] * s, c); // Add the particle to the particle system. AddParticle(p); } } } bool SparkSystem::AnimateParticles(void) { // This function is called once per frame to move the particles. // Times are in milliseconds. long dt = TheTimeMgr->GetDeltaTime(); float fdt = TheTimeMgr->GetFloatDeltaTime(); long particleCount = 0; Particle *particle = GetFirstParticle(); while (particle) { // Get the next particle now in case the current one is removed from the system. Particle *next = particle->nextParticle; long life = (particle->lifeTime -= dt); if (life > 0) { // Update velocity with gravity. particle->velocity.z += K::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; particleCount++; } else { // Particle burned out. FreeParticle(particle); } particle = next; } // If no particles survived, then return false to indicate that it's time to self-destruct. return (particleCount != 0); } GameWorld::GameWorld(const char *name) : World(name), spectatorCamera(2.0F, 1.0F, 0.3F) { // This constructor is called when the Game::ConstructWorld() 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() { } WorldResult GameWorld::Preprocess(void) { // The Preprocess() function is called after the world has been constructed. // We must always call the base class Preprocess() function first. WorldResult result = World::Preprocess(); 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 will have a locator // type of kLocatorSpectator. We only look in the root zone of the world. Zone *zone = GetRootNode(); if (zone) { // Iterate through all of the markers in the zone. const Marker *marker = zone->GetFirstMarker(); while (marker) { MarkerType type = marker->GetMarkerType(); if (type == kMarkerLocator) { if (static_cast<const LocatorMarker *>(marker)->GetLocatorType() == kLocatorSpectator) { // The marker is of the correct type for the spectator camera. // So set the spectator camera's initial position and orientation. const Vector3D direction = marker->GetWorldTransform()[0]; float azimuth = Atan(direction.y, direction.x); float altitude = Atan(direction.z, Sqrt(direction.x * direction.x + direction.y * direction.y)); spectatorCamera.SetCameraAzimuth(azimuth); spectatorCamera.SetCameraAltitude(altitude); spectatorCamera.SetNodePosition(marker->GetWorldPosition()); } } // Get the next marker in the list. marker = marker->Next(); } } else { 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. SetCamera(&spectatorCamera); return (kWorldOkay); } void GameWorld::Render(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::Render(). World::Render(); } Game::Game() : // This is the constructor for the main application/game module class. // This class is constructed by the ConstructApplication() function, // which is called right after the application/game DLL is loaded by // the engine. There is only one instance of this class, so it inherits // from the Singleton template, which we initialize first. Singleton<Game>(TheGame), // The display event handler encapsulates a function that gets called // when the Display Manager changes something like the screen resolution. displayEventHandler(&HandleDisplayEvent), // 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 pre-cache 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 and validation function. // 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. sparkSystemReg(kParticleSystemSpark, "Sparks"), // Locator markers are registered so that the World Editor // can display their names in the Get Info dialog box. locatorReg(kLocatorSpectator, "Spectator Camera") { // This installs an event handler for display events. This is only // necessary if we need to perform some action in response to // display events for some reason. TheDisplayMgr->InstallDisplayEventHandler(&displayEventHandler); // 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. prevEscapeProc = TheInputMgr->GetEscapeProc(); prevEscapeData = TheInputMgr->GetEscapeData(); TheInputMgr->SetEscapeProc(&EscapeProc, 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->SetWorldConstructor(&ConstructWorld); // 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); // Put the Message Manager in single-player mode. TheMessageMgr->BeginSinglePlayerGame(); } Game::~Game() { // When the game DLL is about to be unloaded, this destructor is called. TheWorldMgr->UnloadWorld(); TheWorldMgr->SetWorldConstructor(nullptr); // 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->SetEscapeProc(prevEscapeProc, prevEscapeData); } World *Game::ConstructWorld(const char *name, void *data) { // 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::HandleDisplayEvent(EventType eventType, long param, void *data) { // This function is called when a display event occurs (because we // registered it in the Game constructor). if (eventType == kEventDisplayChange) { // The screen resolution has changed. Handle accordingly. } } void Game::EscapeProc(void *data) { // This function is called when the user hits the escape key in gameplay // mode because we registered it using the InputMgr::SetEscapeProc() function. }

