SimpleBall Source Code: Difference between revisions

From C4 Engine Wiki
Jump to navigation Jump to search
(Created page with "This page contains the complete source code for the <code>SimpleBall</code> example game module. See the Simple Games page for more information about the basic examples. == SimpleBall.h == <syntaxhighlight lang="c++"> #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 ca...")
 
(No difference)

Latest revision as of 21:41, 12 September 2023

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);
}

See Also