Mouse Input - Unit Movement

From C4 Engine Wiki

Jump to: navigation, search

Previous Section: Mouse Input - Unit Selection

Contents

Modifying the Tank Controller

Here, we’ll get the selected tank entity to move by right-clicking an area of the playfield. It’s nothing fancy though. The tank simply rotates in the direction of the clicked waypoint, and travels to that waypoint (Turret rotation will come in later). The majority of our edits will be in Tank.h and Tank.cpp. We’ll be focusing on implementing the Move function. Start by adding the following members and member functions to the TankController class:

<Tank.h>

class TankController : public Controller
{
	private:
		...
		Point3D	waypoint;
 
		float 	curRotation;
		float 	reqRotation;
 
		void MoveTank(Entity *entity, float dt);
		...
	public:
		...
		void SetWaypoint(Point3D point)
		{
			waypoint = point;
		}
		...
};


The waypoint will be the point where the tank wants to be. Setting the waypoint will make the Move function rotate and then move the tank towards the waypoint until it has arrived at the waypoint. curRotation will be used as the current rotation, and reqRotation will be the required or requested rotation that the current rotation should attempt to achieve. The MoveTank function will contain all the work we’ll be doing to modify the rotation and position of the tank, and it is called by the TankController’s Move function.


Next, put in initialization code for these variables in the constructor’s initialization list <Tank.cpp>

TankController::TankController() : 
	...
	curRotation(0), reqRotation(0), 
	waypoint(0.0f, 0.0f, 0.0f)
{
 
}


Although waypoint is initialized to the origin, let’s set the waypoint’s initial location to the tank’s position in the Preprocess function so that when the application starts up, the tank will already be at the waypoint:

<Tank.cpp>

void TankController::Preprocess(void)
{
	Controller::Preprocess();
 
	Node *node = this->GetTargetNode();
	Entity *entity = static_cast<Entity*>(node);
	if(entity)
	{
		waypoint = entity->GetNodePosition();	// <-- Add this line
		...
	}
}


In the Move function, we’ll simply get the current delta time, get the target entity node of the controller, and pass these into MoveTank, and then invalidate the entity.


<Tank.cpp>

void TankController::Move(void)
{
	float dt = TheTimeMgr->GetFloatDeltaTime() * 0.001f;
 
	Node *node = this->GetTargetNode();
	if(node)
	{
		Entity *entity = static_cast<Entity*>(node);
		if(entity)
		{
			MoveTank(entity, dt);
 
			entity->Invalidate();
		}
	}
}


The following is the MoveTank function which does all of the logic to get the tank to move toward the waypoint.

<Tank.cpp>

void TankController::MoveTank(Entity* entity, float dt)
{
	if(!entity || (entity->GetEntityType() != kEntityTank))
		return;
 
	// Get the direction that the tank should be facing
	Vector3D reqDirVector = waypoint - entity->GetNodePosition();
 
	// Get the distance to the waypoint
	float travelDistance = C4::Magnitude(reqDirVector);
 
	reqDirVector.Normalize();
 
	reqRotation = C4::Atan(reqDirVector.y, reqDirVector.x);
 
	float deltaRotation = reqRotation - curRotation;
 
	while(deltaRotation < -K::pi)
		deltaRotation += K::two_pi;
 
	while(deltaRotation > K::pi)
		deltaRotation -= K::two_pi;
 
	bool rotationArrived = true;
 
	// Check if delta rotation is larger than threshold
	if(C4::Fabs(deltaRotation) > 0.1f)
	{
		rotationArrived = false;
 
		if(deltaRotation < 0)
		{
			curRotation -= 1.0f * dt;
		}
		else if(deltaRotation > 0)
		{
			curRotation += 1.0f * dt;
		}
	}
 
	// Start with the current position
	Point3D newPosition = entity->GetNodePosition();
	if(travelDistance > 0.1f && rotationArrived)
	{
		// Advance the position
		newPosition += reqDirVector * 0.5f * dt;
	}
 
	// Start with a transform about the pivot
	Transform4D transform;
	transform.SetRotationAboutZ(curRotation);
	entity->SetNodeTransform(transform);
 
	// Then move the node to the new position
	entity->SetNodePosition(newPosition);
}


reqDirVector represents the difference vector between the current location and the desired location. travelDistance saves the length of this vector, so that reqDirVector can be normalized afterward. This vector is normalized so that we can use the resulting unit vector to generate the required rotation from an arc tangent calculation. deltaRotation stores the amount of rotation needed to get to the required rotation. A couple of while loops get deltaRotation to be within -pi to pi, the range of the arc tangent calculation . An absolute value check is then done to see if the tank is rotated enough. If it isn’t, curRotation is modified to get closer to reqRotation. The new position is then changed only if the desired rotation is met with the rotationArrived flag. The last few steps generate an appropriate visual transformation for the tank entity.

Expanding on the RTS Command Action

If you trudge through all of the math in the MoveTank function, you’ll notice that the tank movement is ultimately controlled by the waypoint variable. If the waypoint doesn’t differ from the tank entity’s current position, no movement takes place. That being said, the waypoint variable is controlled through the RTSCommandAction class, which was also the class used for selecting a tank.

To start off, add a new Action type:

<CommandAction.h>

enum
{
	kActionSelect = 'fire',
	kActionCommand = 'trig'	// <-- new Action type
};

The kActionCommand Action type is issued when the key bound to ‘trig’ is pressed, which defaults to the right mouse button in the engine.cfg file. Modify the Begin function to account for this change. In this function, you’ll find the GetActionType() check:

<CommandAction.cpp>

if(this->GetActionType() == kActionSelect)
{
	EntitySelect();
}

Remove this, and replace it with the switch statement below:

<CommandAction.cpp>

void RTSCommandAction::Begin(void)
{
	if(!clicked)
	{
		// BEGIN: New switch statement
		switch(this->GetActionType())
		{
			case kActionSelect:
				EntitySelect();
				break;
 
			case kActionCommand:
				EntityCommand();
				break;
		}
		// END: New switch statement
 
		clicked = true;
	}	
}

That’s much cleaner now! And finally, add the implementation of EntityCommand as follows:

<CommandAction.h>

class RTSCommandAction : public Action
{
	private:
		...
		void EntityCommand();
		...
};

<CommandAction.cpp>

void RTSCommandAction::EntityCommand()
{
	CombatWorld *world = static_cast<CombatWorld*>(TheWorldMgr->GetWorld());
	if(world)
	{
		RTSCamera *cam = world->GetNavCamera();
		Point3D camPosition = cam->GetNodePosition();
		Point3D curFocus = cam->GetCurrentFocus();
 
		CollisionData data;
		CollisionState state = kCollisionStateNone;
 
		Ray ray;
		Point2D cursorPoint = world->GetCursorPosition();
		cam->GetWorldRayFromPoint(Point(cursorPoint.x, cursorPoint.y), &ray);
 
		float distance = Magnitude(camPosition - curFocus);
		Point3D p1 = ray.origin;
		Point3D p2 = ray.origin + ray.direction * ray.tmax;
 
		state = world->QueryWorld(p1, p2, 0.0f, 0, &data);
 
		if(state != kCollisionStateNone)
		{
			Geometry *geo = data.geometry;
			Node *node = geo->GetSuperNode();
 
			if(node)
			{
				C4::Array<Entity*> *tanks = world->GetEntityTanks();
				for(long i = 0; i < tanks->GetElementCount(); i++)
				{
					Entity *tank = static_cast<Entity*>((*tanks)[i]);
					if(tank)
					{
						TankController *tankController = static_cast<TankController*>(tank->GetController());
						if(tankController && tankController->IsSelected() && cam)
						{
							tankController->SetWaypoint(data.position);
						}
					}	
				}
			}	
		}
	}
}

The first half of the function is largely similar to the IsOverTank function’s first half, in that the function constructs a ray from the camera into the world to act as a collision “probe”. The second half of the function then takes the collision information and iterates through all the entity tanks, compares its geometry with that of the collided geometry, and sets the waypoint to that TankController.

And now, register the new command Action as we did with the select Action:

<RTS.h>

class RTS : public Singleton <RTS>, public Application
{
private:
		...
		RTSCommandAction		*commandAction;
		...
};

In the constructor:

<RTS.cpp>

RTS::RTS(void) : 
...
{
	...
	commandAction = new RTSCommandAction(kActionCommand);
	...
	TheInputMgr->AddAction(commandAction);
	...
}

And in the destructor:

<RTS.cpp>

RTS::~RTS(void) 
{
	...	
	if(commandAction)
		delete commandAction;
	...
}

At this point, the project compiles and you can click and move the tank around. We’re done, right? Well, not quite…

Support Code

As it is right now, there’s a small problem with how we determine the click location in 3D space. The problem is that the collision check from the mouse click queries ALL geometry in the scene for a position to set the waypoint to. That means that the user can potentially click on, say, a wall or another vehicle in the scene, and the waypoint will be set somewhere above or below the intended ground plane.

Here’s an example of what could happen:

Image: rts_example_bad_detection.jpg

Notice that the tank is on top of the wall because the user clicked on top of the wall surface, and that’s where the collision with the mouse cursor occurred. How do we fix this? Well, the key line of code is the call to QueryWorld, more specifically, the fourth parameter in that function. To fix this, we’ll have to utilize exclusion masks for the geometry in the world. What the heck is an exclusion mask? Read on!


The Collision Exclusion Mask

The collision exclusion mask is a value you can set to any geometry or collider to prevent it from being processed by collision detection via the QueryWorld or DetectCollision functions. This value is set by calling the SetCollisionExclusionMask function of the geometry or colllider.

See the API documentation for details of this function here.

For this tutorial, we will pass in the kColliderSightPath bit flag to all objects that will ignore the click command. This will be done below, but before we do that, let’s handle the receiving end. In the QueryWorld function (and the DetectCollision function), the fourth parameter is an unsigned long called clas. The API documentation states this:

“The clas parameter can be used to invalidate certain types of collisions. When a candidate geometry or collider is encountered in the collision detection process, its collision exclusion mask is logically ANDed with the value of the clas parameter. A collision can only occur if the result of this operation is zero.“

This means that if any of the valid flags for this value matches those assigned to any geometry’s collision exclusion mask, the collision for that geometry is ignored. Change the QueryWorld call in the EntityCommand function to exclude geometry and colliders with the kColliderSightPath flag set:

<CommandAction.cpp>

void RTSCommandAction::EntityCommand()
{
	...	
	state = world->QueryWorld(p1, p2, 0.0f, kColliderSightPath, &data);
	...
}

Identifying the Ground

For the unit movement to be complete, we again have to implement some support code. What we need to do is identify the ground geometry and then mark all other geometries as non-collidable for click commands. This is needed so that when the user clicks the mouse on a location, the click is processed only for the ground geometry and no other geometry. If the click is processed for any other geometry, another tank for instance, the click location could potentially end up on top of the tank!

There are several ways that the ground geometry can be marked separately from all other geometry, but the method we’ll use here is to create a new Property called GroundProperty, and assign it to the ground geometry in the World Editor. It’s rather simple, and contains the absolute bare bones required for implementing a Property subclass. All it is used for at this time is so that during the Preprocess step, the world can get a pointer to the ground. (Some other future applications for this property could be settings for things like friction coefficient, damage over time, heal over time, safety zone, hostile territory, etc.). Create the new header and source files for GroundProperty.

<GroundProperty.h>

#ifndef GROUND_PROPERTY_H
#define GROUND_PROPERTY_H
 
#include "C4World.h"
 
namespace C4
{
	enum
	{
		kPropertyGround = 'grnd'
	};
 
	class GroundProperty : public Property
	{
		private:
 
		public:
			GroundProperty();
			~GroundProperty();
 
			static bool ValidNode(const Node *node);
 
			void Pack(Packer& data, unsigned long packFlags) const;
			void Unpack(Unpacker& data, unsigned long unpackFlags);
 
	};
}
#endif

<GroundProperty.cpp>

#include "GroundProperty.h"
 
using namespace C4;
 
GroundProperty::GroundProperty() : Property(kPropertyGround)
{
}
 
GroundProperty::~GroundProperty()
{
}
 
bool GroundProperty::ValidNode(const Node *node)
{
	return (node->GetNodeType() == kNodeGeometry);
}
 
void GroundProperty::Pack(Packer& data, unsigned long packFlags) const
{
	Property::Pack(data, packFlags);
}
 
void GroundProperty::Unpack(Unpacker& data, unsigned long unpackFlags)
{
	Property::Unpack(data, unpackFlags);
}

As with Entities and Controllers, Properties also have to be registered with the application so that they can be used in the World Editor:

<RTS.h>

...
#include "GroundProperty.h"
...
 
class RTS : public Singleton <RTS>, public Application
{
private:
		...	
		PropertyReg<GroundProperty>		groundPropertyReg;
		...
};

And in the initialization list of the RTS constructor, initialize the new property:

<RTS.cpp>

RTS::RTS(void) : 
...
groundPropertyReg(kPropertyGround, "Ground Type")
...
{
	...
}

Now that we have the ability to label which geometry the ground will be, the rest of the code part traverses the scene graph in CombatWorld::Preprocess, finds all geometry that does not have the GroundProperty, and marks them as excluded from collision detection with the kColliderSightPath flag. Here’s the code block for that.

<CombatWorld.cpp>

...
#include “GroundProperty.h”
...
 
WorldResult CombatWorld::Preprocess(void)
{
	WorldResult result = World::Preprocess();
	if (result != kWorldOkay) return (result);
 
	// BEGIN: NEW CODE
	Node *root = this->GetRootNode();
	Node *node = root;
	unsigned int nodeType = 0;
	do
	{
		nodeType = node->GetNodeType();
		if(nodeType == kNodeGeometry)
		{
			Geometry *geo = static_cast<Geometry*>(node);
			if(geo)
			{
				Property *prop = geo->GetProperty(kPropertyGround);
				if(!prop)
				{
					GeometryObject *geoObj = static_cast<GeometryObject*>(geo->GetObject());
					if(geoObj)
					{
						geoObj->SetCollisionExclusionMask(kColliderSightPath);
					}
				}
			}
		}
		node = root->GetNextNode(node);
	}
	while(node);
 
	// END: NEW CODE
 
	SetCamera(&navCamera);
 
	return (kWorldOkay);
}

This is a useful scene graph tree traversal for any sort of preprocessing that needs to be done on nodes during initialization. For now, all it does is set the collision exclusion mask for all geometry objects except for the ground object, but expect several modifications to be done to this traversal in future sections.

Fire up C4, open up rts.wld in the World Editor, select the ground geometry, and click on Node->GetInfo. Switch to the Properties tab, click on Ground Type, and click on Assign.

Image: rts_adding_ground_property.jpg

Save the world and it’s all good to go!

Conclusion

Ah, yet another lengthy section done that seems to not do much. But actually, it wasn’t getting it done that took long to do, but getting it done right. Most of this section was being able to prevent the user from clicking on anything other than the ground. Although there are many ways to accomplish this, a Property was introduced to denote that we’re dealing with a special node, and we were also able to put some initialization code when preprocessing the world.

Next Step

With unit selection and unit movement complete, I think it’s time to bring in an enemy tank unit, and add some turret rotation. This is covered in the next section: Creating an Enemy Unit.

Personal tools