SimpleChar
From C4 Engine Wiki
The SimpleChar 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 SimpleChar 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/Game/game.cfg and changing the value of the variable $gameModuleName to "SimpleChar". To get the primary game module back, change $gameModuleName to "Game" again.
When the SimpleChar module is running, you will be able to run the soldier model around the world using the WSAD keys and the mouse. (To load the world, use the load command in the Command Console. For example, type load world/Lasers to load the Tutorials/world/Lasers.wld world.)
If the movement keys don't seem to work, make sure the menu bar is not visible by pressing the Escape key.
Below is the complete source code for the SimpleChar sample project.
Header File, SimpleChar.h
#ifndef SimpleChar_h #define SimpleChar_h #include "C4World.h" #include "C4Input.h" #include "C4Application.h" #include "C4Interface.h" #include "C4Character.h" #include "C4Engine.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', kActionUse = 'fire' }; // These are movement flags used by the soldier controller. They are set or cleared // by the Begin() and End() functions in the MovementAction class. enum { kMovementForward = 1 << 0, kMovementBackward = 1 << 1, kMovementLeft = 1 << 2, kMovementRight = 1 << 3, kMovementUp = 1 << 4, kMovementDown = 1 << 5, kMovementPlanarMask = 15 }; // Model types are associated with a model resource using the ModelRegistration // class. Models are registered with the engine in the Game constructor. enum { kModelSoldier = 'sold' }; // This is the type for the controller that we use to move the soldier. enum { kControllerSoldier = 'sold' }; // New locator types are registered with the engine in the Game constructor. // The 'spwn' locator is used to specify where the player should be positioned // when a world is loaded. enum { kLocatorSpawn = 'spwn' }; class SoldierController; // 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); }; class UseAction : public Action { public: UseAction(); ~UseAction(); void Begin(void); void End(void); }; // The Interactor class is used to track player interactions with objects in the scene. class SoldierInteractor : public Interactor { private: SoldierController *soldierController; public: SoldierInteractor(SoldierController *controller); ~SoldierInteractor(); void HandleInteractionEvent(InteractionEventType type, Node *node, const Point3D *position); }; // Controllers are used to control anything that moves in the world. // New types of controllers defined by the application/game module can be // registered with the engine when the Game class is constructed. // // This particular controller is used to animate the soldier. It uses the // built-in character controller as a base class so that the engine's native // physics can be used to move the character. class SoldierController : public CharacterController { private: // These are motion states that are used to keep track // of which animation should be played. enum { kMotionNone, kMotionStand, kMotionForward, kMotionBackward }; // The movement flags tell how the user is trying to move the player. unsigned long movementFlags; // The soldier motion keeps track of what animation is currently playing. long soldierMotion; // The azimuth and altitude represent the direction the player is looking // by using the mouse. float modelAzimuth; float modelAltitude; // The frame animator controls playback of an animation resource. FrameAnimator frameAnimator; // The previous center of mass stores the center point of the character on the // previous frame. This is used with the new center point to activate triggers. Point3D previousCenterOfMass; SoldierInteractor soldierInteractor; SoldierController(const SoldierController& soldierController); Controller *Replicate(void) const; void SetSoldierMotion(long motion); public: SoldierController(float azimuth); ~SoldierController(); Model *GetTargetNode(void) const { return (static_cast<Model *>(Controller::GetTargetNode())); } unsigned long GetMovementFlags(void) const { return (movementFlags); } void SetMovementFlags(unsigned long flags) { movementFlags = flags; } float GetModelAzimuth(void) const { return (modelAzimuth); } float GetModelAltitude(void) const { return (modelAltitude); } SoldierInteractor *GetSoldierInteractor(void) { return (&soldierInteractor); } void Preprocess(void); void Move(void); }; // The ChaseCamera class represents a camera that will track the player's movement. class ChaseCamera : public FrustumCamera { private: Model *targetModel; public: ChaseCamera(); ~ChaseCamera(); Model *GetTargetModel(void) const { return (targetModel); } void SetTargetModel(Model *model) { targetModel = model; } void Move(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, a pointer to a spawn locator and an instance of the ChaseCamera // 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: const LocatorMarker *spawnLocator; ChaseCamera chaseCamera; static const LocatorMarker *FindSpawnLocator(const Zone *zone); public: GameWorld(const char *name); ~GameWorld(); const LocatorMarker *GetSpawnLocator(void) const { return (spawnLocator); } ChaseCamera *GetChaseCamera(void) { return (&chaseCamera); } ResourceResult Preprocess(void); void Interact(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 soldierModelReg; LocatorRegistration locatorReg; InputMgr::KeyProc *prevEscapeProc; void *prevEscapeData; MovementAction *forwardAction; MovementAction *backwardAction; MovementAction *leftAction; MovementAction *rightAction; MovementAction *upAction; MovementAction *downAction; UseAction *useAction; SoldierController *soldierController; static World *ConstructWorld(const char *name, void *data); static void HandleDisplayEvent(const DisplayEventData *eventData, void *data); static void EscapeProc(void *data); public: Game(); ~Game(); SoldierController *GetSoldierController(void) const { return (soldierController); } EngineResult LoadWorld(const char *name) override; void UnloadWorld(void) override; }; // 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, SimpleChar.cpp
#include "SimpleChar.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). We respond to // such an event by setting a movement flag in the soldier controller. SoldierController *controller = TheGame->GetSoldierController(); if (controller) controller->SetMovementFlags(controller->GetMovementFlags() | 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). We respond to // such an event by clearing a movement flag in the soldier controller. SoldierController *controller = TheGame->GetSoldierController(); if (controller) controller->SetMovementFlags(controller->GetMovementFlags() & ~movementFlag); } UseAction::UseAction() : Action(kActionUse) { } UseAction::~UseAction() { } void UseAction::Begin(void) { // The player has pressed the fire/use button. If we are currently interacting with // a node in the scene and that node has a controller, then we send an activate event // to that controller to let it know that the player is doing something with it. SoldierController *controller = TheGame->GetSoldierController(); if (controller) { const SoldierInteractor *interactor = controller->GetSoldierInteractor(); const Node *interactionNode = interactor->GetInteractionNode(); if (interactionNode) { Controller *interactionController = interactionNode->GetController(); if (interactionController) { interactionController->HandleInteractionEvent(kInteractionEventActivate, &interactor->GetInteractionPosition(), controller->GetTargetNode()); } } } } void UseAction::End(void) { // The player has released the fire/use button. Let the node with which we are interacting // know that we are done with it by sending its controller a deactivate event. SoldierController *controller = TheGame->GetSoldierController(); if (controller) { const SoldierInteractor *interactor = controller->GetSoldierInteractor(); const Node *interactionNode = interactor->GetInteractionNode(); if (interactionNode) { Controller *interactionController = interactionNode->GetController(); if (interactionController) { interactionController->HandleInteractionEvent(kInteractionEventDeactivate, &interactor->GetInteractionPosition(), controller->GetTargetNode()); } } } } SoldierInteractor::SoldierInteractor(SoldierController *controller) { soldierController = controller; } SoldierInteractor::~SoldierInteractor() { } void SoldierInteractor::HandleInteractionEvent(InteractionEventType type, Node *node, const Point3D *position) { // Always call the base class counterpart. Interactor::HandleInteractionEvent(type, node, position); // If the node with which we are interacting has a controller, // then pass the event through to that controller. Controller *controller = node->GetController(); if (controller) controller->HandleInteractionEvent(type, position); } SoldierController::SoldierController(float azimuth) : CharacterController(kControllerSoldier), soldierInteractor(this) { soldierMotion = kMotionNone; movementFlags = 0; modelAzimuth = azimuth; modelAltitude = 0.0F; } SoldierController::SoldierController(const SoldierController& soldierController) : CharacterController(soldierController), soldierInteractor(this) { soldierMotion = kMotionNone; movementFlags = 0; modelAzimuth = 0.0F; modelAltitude = 0.0F; } SoldierController::~SoldierController() { } Controller *SoldierController::Replicate(void) const { return (new SoldierController(*this)); } void SoldierController::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. CharacterController::Preprocess(); SetRigidBodyFlags(kRigidBodyKeepAwake | kRigidBodyFixedOrientation); SetFrictionCoefficient(0.0F); // We use a frame animator to play animation resources // for the soldier model. Model *soldier = GetTargetNode(); frameAnimator.SetTargetModel(soldier); soldier->SetRootAnimator(&frameAnimator); // Initialize the previous center of mass to the current center of mass // so that this doesn't contain garbage the first time be call ActivateTriggers(). previousCenterOfMass = GetFinalWorldTransform() * GetCenterOfMass(); // Register our interactor with the world. soldier->GetWorld()->AddInteractor(&soldierInteractor); } void SoldierController::Move(void) { // This function is called once per frame to allow the controller to // move its target node. // The movementIndexTable is a 16-entry table that maps all combinations of // the forward, backward, left, and right movement flags to one of eight directions. // The direction codes are as follows: // // 0 - forward // 1 - backward // 2 - left // 3 - right // 4 - forward and left // 5 - forward and right // 6 - backward and left // 7 - backward and right // // The number 8 in the table means no movement, and it appears where either no // movement buttons are being pressed or two opposing buttons are the only ones pressed // (e.g., left and right pressed simultaneously cancel each other out). static const unsigned char movementIndexTable[16] = { 8, 0, 1, 8, 2, 4, 6, 2, 3, 5, 7, 3, 8, 0, 1, 8 }; // First, we grab the mouse deltas from the Input Manager. // We use these to change the angles representing the direction in // which the player is looking/moving. float azm = modelAzimuth + TheInputMgr->GetMouseDeltaX(); if (azm < -K::pi) azm += K::two_pi; else if (azm > K::pi) azm -= K::two_pi; float alt = modelAltitude + TheInputMgr->GetMouseDeltaY(); if (alt < -1.45F) alt = -1.45F; else if (alt > 1.45F) alt = 1.45F; modelAzimuth = azm; modelAltitude = alt; // Now, we determine whether the player is attempting to move, and // we play the appropriate animation on the soldier model. long motion = soldierMotion; Vector2D propel(0.0F, 0.0F); long index = movementIndexTable[movementFlags & kMovementPlanarMask]; if (index < 8) { // The movementDirectionTable maps each direction code looked up in the // movementIndexTable to an angle measured counterclockwise from the straight // ahead direction in units of pi/4. static const float movementDirectionTable[8] = { 0.0F, 4.0F, 2.0F, -2.0F, 1.0F, -1.0F, 3.0F, -3.0F }; float direction = movementDirectionTable[index] * K::pi_over_4 + modelAzimuth; // Set the propulsive force based on the direction of movement. propel = Math::CosSin(direction) * 100.0F; // Determine whether we should play the forward or backward running animation. motion = ((index == 1) || (index >= 6)) ? kMotionBackward : kMotionForward; } else { // No movement flags are set, so we should play the standing animation. motion = kMotionStand; } // Update the external force for the rigid body representing the character. // The GroundContact() function is a member of the CharacterController base class. if (GroundContact()) { SetExternalLinearResistance(Vector2D(10.0F, 10.0F)); SetExternalForce(propel); } else { // If the soldier is not on the ground, reduce the propulsive force down to 2%. // This controls how well the player is able to control his movement while // falling through the air. SetExternalLinearResistance(Vector2D(0.0F, 0.0F)); SetExternalForce(propel * 0.02F); } // Change the soldier's orientation based on horizontal mouse movement. // The SetCharacterOrientation() function is a member of the CharacterController base class. SetCharacterOrientation(modelAzimuth); // If the animation needs to be changed, do it. if (motion != soldierMotion) SetSoldierMotion(motion); // Get the current center of mass and activate triggers along the line connecting to it // from the center of mass in the previous frame. Point3D cm = GetFinalWorldTransform() * GetCenterOfMass(); Model *model = GetTargetNode(); model->GetWorld()->ActivateTriggers(previousCenterOfMass, cm, 0.25F, model); previousCenterOfMass = cm; // Call the Model::Animate() function to update the animation playing for the model. GetTargetNode()->Animate(); } void SoldierController::SetSoldierMotion(long motion) { // This function sets the animation resource corresponding to // the current type of motion assigned to the soldier. Interpolator *interpolator = frameAnimator.GetFrameInterpolator(); if (motion == kMotionStand) { frameAnimator.SetAnimation("soldier/Stand"); interpolator->SetMode(kInterpolatorForward | kInterpolatorLoop); } else if (motion == kMotionForward) { frameAnimator.SetAnimation("soldier/Run"); interpolator->SetMode(kInterpolatorForward | kInterpolatorLoop); } else if (motion == kMotionBackward) { frameAnimator.SetAnimation("soldier/Backward"); interpolator->SetMode(kInterpolatorForward | kInterpolatorLoop); } soldierMotion = motion; } ChaseCamera::ChaseCamera() : FrustumCamera(2.0F, 1.0F) { targetModel = nullptr; } ChaseCamera::~ChaseCamera() { } void ChaseCamera::Move(void) { Model *model = GetTargetModel(); if (model) { CollisionData data; SoldierController *controller = static_cast<SoldierController *>(model->GetController()); // Here, we calculate the local coordinate frame for the chase camera // based on the direction that the player is looking. Vector2D t = Math::CosSin(controller->GetModelAzimuth()); Vector2D u = Math::CosSin(controller->GetModelAltitude()); Vector3D view(t.x * u.x, t.y * u.x, u.y); Vector3D right(t.y, -t.x, 0.0F); Vector3D down = view % right; // We are going to place the camera behind the player, but we don't // want the camera to go through any geometry, so we'll do a quick // check for a collision. const Point3D& position = model->GetWorldPosition(); Point3D p1(position.x, position.y, position.z + 1.5F); Point3D p2 = p1 - view * 4.0F; if (GetWorld()->DetectCollision(p1, p2, 0.3F, kCollisionCamera, &data)) { // There's something in the way, so move the camera in closer // to the player. float t = data.param; p2 = p1 * (1.0F - t) + p2 * t; } // Set the camera's position and orientation. SetNodeTransform(right, down, view, p2); } } GameWorld::GameWorld(const char *name) : World(name) { // 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. spawnLocator = nullptr; } GameWorld::~GameWorld() { } const LocatorMarker *GameWorld::FindSpawnLocator(const Zone *zone) { // Iterate through all of the markers in the zone. const Marker *marker = zone->GetFirstMarker(); while (marker) { MarkerType type = marker->GetMarkerType(); if (type == kMarkerLocator) { const LocatorMarker *locator = static_cast<const LocatorMarker *>(marker); if (locator->GetLocatorType() == kLocatorSpawn) return (locator); } // Get the next marker in the list. marker = marker->Next(); } // Look in all of the subzones. const Zone *subzone = zone->GetFirstSubzone(); while (subzone) { const LocatorMarker *locator = FindSpawnLocator(subzone); if (locator) return (locator); subzone = subzone->Next(); } return (nullptr); } ResourceResult GameWorld::Preprocess(void) { // The Preprocess() function is called after the world has been constructed. // We must always call the base class Preprocess() function first. If it returns // an error, then we just return the same result code. ResourceResult result = World::Preprocess(); if (result != kResourceOkay) return (result); // The world is now completely loaded. We search for a locator node that // represents the player's spawn position. It will have a locator // type of kLocatorSpawn. spawnLocator = FindSpawnLocator(GetRootNode()); return (kResourceOkay); } void GameWorld::Interact(void) { // The Interact() function is called once per frame. Before calling the base // class's Interact() function, we set up the interaction probe to be a line // segment extending two meters from the players head in the direction that // the camera is looking. SoldierController *controller = TheGame->GetSoldierController(); const Point3D& p = controller->GetTargetNode()->GetWorldPosition(); Point3D position(p.x, p.y, p.z + 1.5F); const Vector3D& direction = chaseCamera.GetWorldTransform()[2]; controller->GetSoldierInteractor()->SetInteractionProbe(position, position + direction * 2.0F); // Always call the base class counterpart. World::Interact(); } 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 kModelSoldier type with the // model named "Data/*/soldier/Soldier.mdl". The fourth parameter tells the // engine to pre-cache the model resource and not to display the model in // the World Editor. The last parameter specifies the default controller // type to assign to models of type kModelSoldier. soldierModelReg(kModelSoldier, nullptr, "soldier/Soldier", kModelPrecache | kModelPrivate, kControllerSoldier), // Locator markers are registered so that the World Editor // can display their names in the Get Info dialog box. locatorReg(kLocatorSpawn, "Spawn Location") { // 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 // move the player around and interact with objects. forwardAction = new MovementAction(kActionForward, kMovementForward); backwardAction = new MovementAction(kActionBackward, kMovementBackward); leftAction = new MovementAction(kActionLeft, kMovementLeft); rightAction = new MovementAction(kActionRight, kMovementRight); upAction = new MovementAction(kActionUp, kMovementUp); downAction = new MovementAction(kActionDown, kMovementDown); useAction = new UseAction; // 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); TheInputMgr->AddAction(useAction); // Let the Interface Manager determine when to change input devices to gameplay mode. TheInterfaceMgr->SetInputManagementMode(kInputManagementAutomatic); soldierController = nullptr; } Game::~Game() { // When the game DLL is about to be unloaded, this destructor is called. TheWorldMgr->UnloadWorld(); TheWorldMgr->SetWorldConstructor(nullptr); delete useAction; 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(const DisplayEventData *eventData, void *data) { // This function is called when a display event occurs (because we // registered it in the Game constructor). if (eventData->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. } EngineResult Game::LoadWorld(const char *name) { // Attempt to load the world. WorldResult result = TheWorldMgr->LoadWorld(name); if (result == kWorldOkay) { GameWorld *world = static_cast<GameWorld *>(TheWorldMgr->GetWorld()); const LocatorMarker *locator = world->GetSpawnLocator(); if (locator) { // If a spawn locator was found in the world, put a soldier character there. // The BeginSinglePlayerGame() function puts the Message Manager in single player mode. TheMessageMgr->BeginSinglePlayerGame(); // Calculate the angle corresponding to the direction the character is initially facing. const Vector3D direction = locator->GetWorldTransform()[0]; float azimuth = Atan(direction.y, direction.x); // Load a soldier model and attach a controller to it. Model *model = Model::Get(kModelSoldier); SoldierController *controller = new SoldierController(azimuth); model->SetController(controller); TheGame->soldierController = controller; // Put the model in the world inside the locator's zone. Zone *zone = locator->GetOwningZone(); model->SetNodePosition(zone->GetInverseWorldTransform() * locator->GetWorldPosition()); zone->AddNewSubnode(model); // Set the world's current camera to be our chase camera. // The world will not render without a camera being set. ChaseCamera *camera = world->GetChaseCamera(); camera->SetTargetModel(model); world->SetCamera(camera); } } return (result); } void Game::UnloadWorld(void) { TheWorldMgr->UnloadWorld(); TheMessageMgr->EndGame(); TheGame->soldierController = nullptr; }
