Custom Controller Example: How to get an Enemy to Follow a given (editable) Path
From C4 Engine Wiki
Note: This tutorial has been updated to work with 2.7.1
With the code below, you can create a Path in C4 and assign a Model or Geometry to it, such that the Model or Geometry will follow along the Path. The code is contained in a class called PathController. It has simply been added to the SimpleBall tutorial code below.
To use this code, follow these steps
- Go to your C4/GameCode folder
- Save a copy of the SimpleBall.h and SimpleBall.cpp files there
- Now replace the code in SimpleBall.h and Simpleball.cpp with the code below
- Compile and then start C4
- In the World Editor, open a tutorial world like the Water world
- Open the Paths page from the menu and draw a path in the top viewport
- With the path still selected, press Ctrl-I to edit the node info
- Change the node name to EnemyPath
- Now draw any Geometry, say a cube, again in the top viewport. Raise it slightly above the ground.
- With the Geometry still selected, press Ctrl-I to edit the node info
- Change the Controller for that Geometry to be the new Path Controller
- Press Ctrl-P to save and play your world
You should now see your geometry moving along the path you defined.
Replace the code in SimpleBall.h with this. I simply added the PathController class:
#ifndef SimpleBall_h #define SimpleBall_h #include "C4Application.h" #include "C4Input.h" #include "C4Interface.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', kControllerPath = 'path' }; // 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 { private: BallController(const BallController& ballController); Controller *Replicate(void) const; public: BallController(); ~BallController(); static bool ValidNode(const Node *node); void Preprocess(void); RigidBodyStatus HandleNewRigidBodyContact(const RigidBodyContact *contact, RigidBodyController *contactBody); }; //Path Controller //added for Custom Path Controller class PathController : public Controller { private: float pathRate; // In radians per millisecond String<30> pathName; //path name - default is "Enemy Path" Transform4D originalTransform; // The target's original transform Vector3D originalView; // The direction turret is facing before updating each frame bool lookedForEnemyPath; float crashCount; bool planeHealthAdded; long runCount, loopCount; Array<Point3D> pathPoint; int pointCount; int pathPointCount; QuadWidget *planeHealth;//to show damage to plane PathController(const PathController& pathController); Controller *Replicate(void) const; public: int damage;//damage to Model PathController(); PathController(float rate); ~PathController(); float GetPathRate(void) const { return (pathRate); } void SetPathRate(float rate) { pathRate = rate; } static bool ValidNode(const Node *node); // Serialization functions unsigned long GetPackSize(unsigned long packFlags) const; void Pack(Packer& data, unsigned long packFlags) const; void Unpack(Unpacker& data, unsigned long unpackFlags); // User interface functions long GetSettingCount(void) const; Setting *GetSetting(long index) const; void SetSetting(const Setting *setting); void Preprocess(void); // The function that moves the target node void Move(void); void LookForEnemyPath(void); }; // The SparkSystem class implements a simple particle system that // creates a small burst of sparks. class SparkSystem : public LineParticleSystem { // 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 override; public: SparkSystem(long count); ~SparkSystem(); void Preprocess(void); bool AnimateParticles(void); }; // The StartWindow class is a simple example of a window that handles a button click. // We add the Singleton base class so that a pointer to the window can be tracked easily. class StartWindow : public Window, public Singleton<StartWindow> { private: WidgetObserver<StartWindow> startButtonObserver; void HandleStartButtonEvent(Widget *widget, const WidgetEventData *eventData); public: StartWindow(); ~StartWindow(); void Preprocess(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; ControllerReg<PathController> pathControllerReg;//Path Controller 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(const DisplayEventData *eventData, 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; // 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
And replace the code in SimpleBall.cpp with this. I simply added the implementation for the PathController class:
#include "SimpleBall.h" #include "C4Configuration.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; // This is the definition of the pointer to the StartWindow class singleton. // It should be initialized to nullptr, and its value will be set by // the StartWindow class constructor. StartWindow *C4::TheStartWindow = 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(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 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 set the ball's // coefficient of restitution. RigidBodyController::Preprocess(); SetRestitutionCoefficient(0.95F); } 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 = node->GetNodeTransform() * 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 for line particles // and tell it where the particle pool is. LineParticleSystem(kParticleSystemSpark, &particlePool), // 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 for line particles // and tell it where the particle pool is. LineParticleSystem(kParticleSystemSpark, &particlePool), // 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. LineParticleSystem::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 ConstVector2D *trig = Math::GetTrigTable(); long count = sparkCount; for (machine 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. float speed = Math::RandomFloat(0.004F); Vector2D csp = trig[Math::Random(128)] * speed; const Vector2D& cst = trig[Math::Random(256)]; p->velocity.Set(cst.x * csp.y, cst.y * csp.y, csp.x); // 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); } // The StartWindow constructor initializes the Window base class with the name of the panel // resource to load. The startButtonObserver member is initialized with the functions that // is called when the "Start" push button in the window posts an activate event. StartWindow::StartWindow() : Window("panel/SimpleBall"), Singleton<StartWindow>(TheStartWindow), startButtonObserver(this, &StartWindow::HandleStartButtonEvent) { } StartWindow::~StartWindow() { } void StartWindow::Preprocess(void) { // We must call the Window base class Preprocess() function first to initialize the // internal structures that are used to search for widgets. Window::Preprocess(); // Find the push button widget named "Start" and assign our observer to it. Widget *button = FindWidget("Start"); button->SetObserver(&startButtonObserver); } 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 start window and load a world to play. delete this; TheWorldMgr->LoadWorld("world/SimpleBall"); } } 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"), pathControllerReg(kControllerPath, "Path"), // 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); // 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->SetWorldConstructor(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->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. } //Adding Custom Path Controller PathController::PathController() : Controller(kControllerPath) { // Set a default value for the Path rate pathRate = 1.0f;//Corresponds to the speed of object along the path pathName = "EnemyPath";//the default name of the path this object should follow } PathController::~PathController() { if(planeHealth != nullptr) delete planeHealth; } PathController::PathController(const PathController& pathController) : Controller(pathController) { pathRate = pathController.pathRate; pathName = "EnemyPath"; } Controller *PathController::Replicate(void) const { return (new PathController(*this)); } unsigned long PathController::GetPackSize(unsigned long packFlags) const { unsigned long size = Controller::GetPackSize(packFlags); // Add enough space to hold a float for the path rate, the path name as well as // the target node's original transform unsigned long length = size + 8 + sizeof(pathName) + sizeof(Transform4D) - sizeof(float); return (length); } void PathController::Pack(Packer& data, unsigned long packFlags) const { Controller::Pack(data, packFlags); // Write the Path rate data << pathRate; //Write the Path Name data << pathName; // Write the original transform data << originalTransform; } void PathController::Unpack(Unpacker& data, unsigned long unpackFlags) { Controller::Unpack(data, unpackFlags); // Read the Path rate data >> pathRate; //read the Path Name data >> pathName; // Read the original transform data >> originalTransform; } long PathController::GetSettingCount(void) const { // There are two settings return (2); } Setting *PathController::GetSetting(long index) const { // Is it asking for the first setting? if (index == 0) { // Yes, return a new text setting and set its value return (new TextSetting('rate', Text::FloatToString(pathRate), "Speed on Path", 7, &EditTextWidget::FloatNumberFilter)); } // Or is it asking for the second setting? else if (index == 1) { // Yes, return a new text setting and set its value return (new TextSetting('pnme', pathName, "Path Name", 20)); } return (nullptr); } void PathController::SetSetting(const Setting *setting) { // Are we setting the Path rate? if (setting->GetSettingIdentifier() == 'rate') { // Yes, grab the value from the setting const char *text = static_cast<const TextSetting *>(setting)->GetText(); pathRate = Text::StringToFloat(text); } //or are we setting the path name? else if (setting->GetSettingIdentifier() == 'pnme') { // Yes, grab the value from the setting const char *text = static_cast<const TextSetting *>(setting)->GetText(); pathName = text; } } bool PathController::ValidNode(const Node *node) { return (node->GetNodeType() == kNodeModel || node->GetNodeType() == kNodeGeometry); } void PathController::Preprocess(void) { Controller::Preprocess(); planeHealth = nullptr; damage = 0; crashCount = 0.0F; planeHealthAdded = false; // Grab the original transform of the target node const Node *target = GetTargetNode(); originalTransform = target->GetNodeTransform(); pointCount = 0; pathPointCount = 0; lookedForEnemyPath = false; Point3D originalDirection = Point3D(-originalTransform[1].x,-originalTransform[1].y,-originalTransform[1].z); originalView = (originalDirection).Normalize(); // Set the kGeometryDynamic flag for any geometry nodes const Node *node = target; do { if (node->GetNodeType() == kNodeGeometry) { // Node is a geometry, so grab its object GeometryObject *object = static_cast<const Geometry *>(node)->GetObject(); // Set the kGeometryDynamic flag object->SetGeometryFlags(object->GetGeometryFlags() | kGeometryDynamic); } // Iterate through entire subtree node = target->GetNextNode(node); } while (node); } void PathController::Move(void) { Node *target = GetTargetNode(); Transform4D translation; Point3D myTranslation; //Look for the enemy path once only if(!lookedForEnemyPath) LookForEnemyPath(); myTranslation.x = pathPoint[pathPointCount].x; myTranslation.y = pathPoint[pathPointCount].y; myTranslation.z = pathPoint[pathPointCount].z - crashCount;//plane falls down if crashing pathPointCount++; translation.SetIdentity(); translation.SetTranslation(myTranslation); if (pathPointCount >= pointCount) pathPointCount = 0; target->SetNodeTransform(originalTransform * translation); //Orientation - face in the direction of motion if (pathPointCount > 0) { Vector3D view = (pathPoint[pathPointCount] - pathPoint[pathPointCount-1]); //view.z -= crashCount/20.0f;//nose dives down if crashing view = view.Normalize(); float x = view.x; float y = view.y; float f = InverseSqrt(x * x + y * y); Vector3D right(y * f, -x * f, 0.0F); Vector3D down = view % right; float bankAngle = 0.0f;//temp - replace by variance with path curvature right = right.RotateAboutAxis(bankAngle + crashCount, view);//spins if crashing down = down.RotateAboutAxis(bankAngle + crashCount, view);//spins if crashing target->SetNodeMatrix3D(view, -right, -down); } if ((GetTargetNode()->GetNodePosition()).z <= 0.0f) delete target; else target->Invalidate(); } void PathController::LookForEnemyPath(void) { float x=0.0f, y=0.0f, z=0.0f, distance = 0.0f; const float EPSILON = 0.1f; if (pathRate < EPSILON) pathRate = EPSILON; const int MAX_ITERATIONS = 10; const char * stringOutput = " "; if (pathRate < EPSILON) pathRate = EPSILON + 0.1; Node *root = TheWorldMgr->GetWorld()->GetRootNode(); // start at the root Node *thisnode = root; PathMarker *pathMarker = nullptr; do { stringOutput = " "; if (thisnode->GetNodeName() != NULL) stringOutput = thisnode->GetNodeName(); if (pathName == stringOutput) { //Call the Path Node "EnemyPath" in the Editor Marker *marker = static_cast<Marker *>(thisnode); if (marker->GetMarkerType() == kMarkerPath) pathMarker = static_cast<PathMarker *>(marker); const Path *path = pathMarker->GetPath(); const PathComponent *component = path->GetFirstPathComponent(); while (component) { pathPoint.AddElement(component->GetPosition(0)); pointCount++; float t = 0.5f, t_old = 0.0f, speed = 0.0f, distance, x, y, z;//0.03125F; while (t < 1.0f && pointCount < 100000)//limit max length of path { float diff = 0.0f; bool found = false; int iterations = 0; pathPoint.AddElement(component->GetPosition(t)); //tangent.AddElement(Vector3D(pathPoint[pointCount] - pathPoint[pointCount-1])); do { pathPoint[pointCount] = component->GetPosition(t); Vector3D tangent = (pathPoint[pointCount] - pathPoint[pointCount-1]); //tangent[pointCount-1] = Vector3D((pathPoint[pointCount] - pathPoint[pointCount-1])); x = tangent.x; y = tangent.y; z = tangent.z; distance = Sqrt(x * x + y * y + z * z); speed = distance / (t - t_old); //length += distance; diff = distance - pathRate;//targetDistance; //Using Newton's method to converge on t for target distance along path if (fabs(diff) > EPSILON && iterations++ < MAX_ITERATIONS) { t -= diff/speed; if (fabs(t) >= 1.0f) t = t_old + 0.05f; } else { if (iterations >= MAX_ITERATIONS) //did'nt converge { t = t_old + t_old/(float)pointCount; if (fabs(t) > 1.0f) t = 1.0f; pathPoint[pointCount] = component->GetPosition(t); //tangent[pointCount-1] = Vector3D(pathPoint[pointCount] - pathPoint[pointCount-1]); } iterations = 0; found = true; t_old = t; t += t/(float)pointCount;//2.0f; //length += distance; pointCount++; } } while (!found); } component = component->Next(); lookedForEnemyPath = true;//found enemyPath at this point } } thisnode = root->GetNextNode(thisnode); } while (thisnode && !lookedForEnemyPath); lookedForEnemyPath = true;//After checking all node set to true to avoid checking each frame!! }
