Game Development Community

dev|Pro Game Development Curriculum

Flash Teams (3 view + teleport)

by Matthew Franklin · 09/20/2002 (9:47 am) · 6 comments

I've been toying with a new gameplay mod that seems quite fun for CtF, and is especially well suited for a graphics level that "under-demands" from its target hardware level. In this gameplay type, each player sees smaller views of what his teammates are seeing. A key is bound for each view, which allows the player to teleport to that location at any time. The tradeoff between breadth of coverage and backup in a firefight gives this strategic depth.

This is a work in progress. I've got to test it with: proper CtF; teleport timers (i.e. only once every 30 seconds); bots that understand the concept; a hotkey to make your view flash on your teammates' displays; a couple code optimizations; pretty UI frames; more views; and so on. Feel free to critique-- I haven't been torquing long, and can use the constructive criticism.

The teleport code is mostly taken from Stefan Beffy Moises' teleport tutorial on this site. Thanks!

Alright, so here's the C++ code. Brief and simple.
Header file:
#ifndef _SUBVIEWTSCTRL_H_
#define _SUBVIEWTSCTRL_H_

#include "gameTSCtrl.h"

//----------------------------------------------------------------------------
class SubviewTSCtrl: public GameTSCtrl 
{
private:
	/// The shape whose camera we're displaying.
	ShapeBase* mSourceShape;
	/// Cached image of last render result.
	TextureHandle mTextureHandle;
	/// We change the bmp contents so often, it makes sense to keep this around.  Generally, you can go
	/// through mTextureHandle.
	GBitmap* mCachedRender;

	/** GUI/console variables **/
	/// Currently just used to see if we're active (rarely used, since visible handles that most of the time); 
	/// possibly useful for lookup in the future.
	S32 mClientId;
	/// Lookup key-- find the client with this name and display its camera.
	const char *mClientName;
	/// Used to determine if its our time to paint.  If not unique, multiple views will paint in the same frame.
	S32 mSubviewId;

	void grabImage();
	void ensureCurrentBitmap();
	bool findSourceShape(GameConnection *connection);

public:
	SubviewTSCtrl();

	static void initPersistFields();
	bool processCameraQuery(CameraQuery *query);
	bool getSubviewCameraTransform(MatrixF* mat, ShapeBase *obj);
	void onRender(Point2I offset, const RectI &updateRect);
	~SubviewTSCtrl();
	DECLARE_CONOBJECT(SubviewTSCtrl);
};

#endif

.cc file:
#include "game/gameTSCtrl.h"
#include "console/consoleTypes.h"
#include "game/projectile.h"
#include "game/gameBase.h"
#include "game/gameConnection.h"
#include "game/shapeBase.h"
#include "game/player.h"
#include "game/subviewTSCtrl.h"
#include "platform/profiler.h"
#include <string.h>
// for rtti
//#include <typeinfo.h>

//----------------------------------------------------------------------------
// Class: SubviewTStrl
//
// Use via a SubviewTSCtrl component in your GUI.  Set its clientId and clientName
// to the appropriate client, and it will render that client's camera.
// Only renders every nth frame, where n is the number of SubviewTSCtrls.  Give
// each SubviewTSCtrl a unique SubviewId in your gui to make it render on its own
// frame, and save yourself the frame hit.
//----------------------------------------------------------------------------
IMPLEMENT_CONOBJECT(SubviewTSCtrl);

/// Number of frames per update of any subview.  1 means a subview updates each frame;
/// 3 means a subview only updates every 3 frames.  The bitmap render cache doesn't
/// seem to show a helpful improvement if this value is 1; 2 or 3 is great.
#define FRAMES_PER_UPDATE 3

// TODO: yank these; just use max bounds of existing subviews.
#define MAX_EXTENT_X 300
#define MAX_EXTENT_Y 300

/// Number of SubviewTSCtrls in existence.
static int subviewCounter = 0;
/// Internal frame counter used for deciding which subview to paint.
static int frameCounter = 0;

/// For pixel grabbing from the back buffer for all subviews, to avoid repeated memory allocation.
static U8 * pixelGrabBuffer = new U8[MAX_EXTENT_X * MAX_EXTENT_Y * 3];

SubviewTSCtrl::SubviewTSCtrl() : mClientId(0), mClientName(""), mSubviewId(-1), mTextureHandle(NULL), mCachedRender(NULL), mSourceShape(NULL)
{
	subviewCounter++;
}

/// Overridden to return the camera of our source shape, which is a client on our team.
bool SubviewTSCtrl::processCameraQuery(CameraQuery *camq)
{
	GameConnection * connection = GameConnection::getServerConnection();
	if(connection)
	{
		// If we don't have a shape from which to get the camera or it's out of date, and we can't find a good one, bail.
		if((!mSourceShape || dStrcmp(mSourceShape->getShapeName(), mClientName)) && !findSourceShape(connection)) 
			return false;
		float cameraPos = 0;
		mSourceShape->getCameraTransform(&cameraPos, &camq->cameraMatrix);
		camq->object = this;
		camq->nearPlane = 0.2f;
		camq->fov = mSourceShape->getCameraFov();
		return true;
	}
	return false;
}

/// Overridden to skip all the stuff we don't need, but more importantly, to render our
/// cached bitmap in off frames.  
void SubviewTSCtrl::onRender(Point2I offset, const RectI &updateRect)
{
	if(mClientId <= 0) return;
	// One subview keeps track of frames...
	if(mSubviewId == 0) frameCounter++;
	if(frameCounter >= subviewCounter*FRAMES_PER_UPDATE) frameCounter = 0;

	if(mClientId && mSubviewId*FRAMES_PER_UPDATE != frameCounter && mTextureHandle)
	{
		// This is an off frame, render the bitmap.
		PROFILE_START(BitmapDraw);
		dglClearBitmapModulation();
		dglDrawBitmap((TextureObject *)mTextureHandle, Point2I(mBounds.point.x, mBounds.point.y), GFlip_Y);
		PROFILE_END();
	}
	else if(mClientId)
	{
		// It's our turn to render.  Do so, then store the result for future paints.
		PROFILE_START(RenderDraw);
		GuiTSCtrl::onRender(offset, updateRect);
		grabImage();
		PROFILE_END();
	}
}

/// Gets values from the GUI.
/// Overridden to handle our own fields: clientId and clientName for knowing whose view to paint, and
/// subviewId to determine which frame we paint on.
void SubviewTSCtrl::initPersistFields()
{
   Parent::initPersistFields();
   addField("clientId", TypeS32, Offset(mClientId, SubviewTSCtrl));
   addField("clientName", TypeString, Offset(mClientName, SubviewTSCtrl));
   addField("subviewID", TypeS32, Offset(mSubviewId, SubviewTSCtrl));
}

/************ private methods ******************/

/// Searches for the client specified in the GUI.
/// @return true if the client was found, in which case we now have a shape with its camera.
bool SubviewTSCtrl::findSourceShape(GameConnection *connection)
{
	for (SimSetIterator itr(connection); *itr; ++itr)
	{
		if ((*itr)->getType() & ShapeBaseObjectType)
		{
			ShapeBase* shape = static_cast<ShapeBase*>(*itr);
			if(shape->getShapeName() && !strcmp(shape->getShapeName(), mClientName))
			{
				mSourceShape = shape;
				return true;
			}
		}
	}
	return false;
}

/// Decomp util method: just checks the bounds of our current bitmap, and if it's different from our own bounds,]
/// makes a new one to hold render results.  Does nothing if the bitmap is the correct size.
void SubviewTSCtrl::ensureCurrentBitmap()
{
	if(!mCachedRender || mCachedRender->width != mBounds.extent.x || mCachedRender->height != mBounds.extent.y)
	{
		// Bitmap is out of date; make a new one.  Don't need to delete the old; TextureHandle does that.
		mCachedRender = new GBitmap();
		mCachedRender->allocateBitmap(U32(mBounds.extent.x), U32(mBounds.extent.y));
	}
}

/// Pulls the image from the offscreen buffer right after a paint, and stores it in a TextureHandle for painting later.
void SubviewTSCtrl::grabImage()
{
	ensureCurrentBitmap();
	glReadBuffer(GL_BACK);
	glReadPixels(1, 25, mBounds.extent.x, mBounds.extent.y, GL_RGB, GL_UNSIGNED_BYTE, pixelGrabBuffer);
    dMemcpy(mCachedRender->getAddress(0, 0), pixelGrabBuffer, U32(mBounds.extent.y * mBounds.extent.x * 3));
	// TextureHandle (well, really, TextureManager) handles deleting the old entry if mClientName clobbers it.
	mTextureHandle = TextureHandle(mClientName, mCachedRender);
}

SubviewTSCtrl::~SubviewTSCtrl()
{
	// none needed currently
}

Alright, so most of it's pretty straightforward and well-commented. But what the heck am I doing with all those counters and the bitmap? You're now rendering 3 views (and in your own larger team version, maybe 4 or 5 or 6-- 2 teammates isn't all that fun!). But a smooth framerate is only really important on the main view; teammate views can update 10 times a second and no one will care. So I've rigged it so that only one SubviewTSCtrl renders per frame (or several frames); the rest display a bitmap that they've cached. This really helps your framerate (to test it out, disable this functionality-- it can be brutal with a few views).

Okay, now here's the bulk of the scripting changes in one file:

addMessageCallback('MsgClientJoin', handleClientJoin);
addMessageCallback('MsgClientDrop', handleClientDrop);

$CurrentClientId = 0;

function handleClientJoin(%msgType, %msgString, %clientName, %clientId)
{
if(StrStr(%msgString, "Welcome") != -1) {
$CurrentClientId = %clientId;
}
if($CurrentClientId != %clientId)
{
if(SubviewA.clientId == 0)
{
SubviewA.clientId = %clientId;
SubviewA.clientName = detag(%clientName);
SubviewA.visible = "1";
}
else if(SubviewB.clientId == 0)
{
SubviewB.clientId = %clientId;
SubviewB.clientName = detag(%clientName);
SubviewB.visible = "1";
}
}
}

function handleClientDrop(%msgType, %msgString, %clientName, %clientId)
{
if(SubviewA.clientId == %clientId)
{
SubviewA.clientId = "0";
SubviewA.clientName = "";
SubviewA.visible = "0";
}
if(SubviewB.clientId == %clientId)
{
SubviewB.clientId = "0";
SubviewB.clientName = "";
SubviewB.visible = "0";
}
}
}

Make sure to add that so it gets used by your game.
I added this line to init.cs:
exec("./scripts/subviews.cs");

Alright, you're all done with the subviews. Now you just need to add keys to make you teleport. I make this really simple; you might want to use some decomp if you prefer your code to be maintainable. :)

I added these to default.bind.cs; you might choose a different place:

moveMap.bind(keyboard, "t", teleportPlayerA);
moveMap.bind(keyboard, "g", teleportPlayerB);

unction teleportPlayerA(%val)
{

if (%val)
commandToServer('TeleportPlayerA');
}
function teleportPlayerB(%val)
{
if (%val)
commandToServer('TeleportPlayerB');
}

Here's what I added to commands.cs:

function serverCmdTeleportPlayerA(%client)
{
%player = %client.player;
%currPlayerPos = %player.getPosition();
%targetPos = SubviewA.clientId.player.getPosition();
%x = getWord(%targetPos, 0);
%y = getWord(%targetPos, 1);
%z = getWord(%targetPos, 2);
// This seems to be the proper Z to make you stand on his head
%z += 3.0;
%finalPos = %x SPC %y SPC %z;
%player.setTransform(%finalPos);
}
[/code]
You'll need a copy of the above function for every possible subview, or simply one decomped function that gets called with a different argument.

And finally, you'll need to change this line in player.cs:
renderFirstPerson = false;
becomes
renderFirstPerson = true;

Otherwise, you're invisible to yourself in your subviews.

That's it for the basic mod! It requires teams to be fun. I plan to integrate it with one of the teamplay mods from this site, and hopefully with a CtF mod if I can find one, as well as some good bots.

ONE BUG: this doesn't deal well with resolutions other than 800x600, likely due to my magic numbers (where did that 25 come from?). I've got to figure out how to properly render at the appropriate position (I assume coordinates are based on a 640x480 view). I'll edit this soon with a fix.

#1
09/25/2002 (6:55 am)
How much FPS does this cost? At first glance I would say tons, but if you can surprise me, I'd be very happy.
#2
09/25/2002 (9:48 am)
Well, you'd be right-- you're rendering another view per frame (although no more than one extra view per frame, as mentioned under the C++ code). So it's not quite as bad as cutting your framerate in half, but it's close. There are some optimizations that might help; I've yet to look through the engine code and see which ones are practical to turn off just for a single render.

But currently, yes, this is a project I came up with because I had a little extra horsepower. :) I was inspired by the latest Indie Game Jam thing, where they started tossing millions of sprites in-- what if we could have millions of 3D objects, or all the views we want? Well, obviously it's not going to happen commercially, but an engine like this or a game like Tribes 2 gets 100fps on modern systems. So putting this sort of gameplay in and sacrificing graphics is doable on a hobby level; just don't expect to see it in Epic's latest FPS.
#3
10/03/2002 (12:24 pm)
In tribes 2 the command map had a little camera that you could obs people through, and it's detail was awful but you couldn't tell because it was so small. If you did something similar to this, you could have 4 windows, 2 big ones and 2 smaller ones. The big ones could have like 50-80% normal LOD, and the small ones could omit a lot. You could cover it up further by maybe making them static-ey, just an idea. It's a really neat idea you have.
#4
02/13/2006 (10:56 am)
can you teleport to another camera view rather than another player's view ???
#5
02/13/2006 (11:21 am)
You can't really do much. :) This early experiment with the engine has been abandoned (I'm focusing on 2D games, given my team size), and probably isn't even relevant to the latest TGE release. If you're looking to do something like this, I'd recommend re-implementing it yourself, but with this code as a potential guide to what methods to start looking at.
#6
03/02/2006 (2:56 pm)
great work!!!!!!!

this is a perfect source to point me int he right direction (fps, with a 3rd person veiw of myself in the corner (like to jump from and to stuff etc.) this is amazing well done!