//----------------------------------------------------------------------------------- // // Torque Network Library - TNLTest example program // Copyright (C) 2004 GarageGames.com, Inc. // For more information see http://www.opentnl.org // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // For use in products that are not compatible with the terms of the GNU // General Public License, alternative licensing options are available // from GarageGames.com. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // //------------------------------------------------------------------------------------ #include #include "testGame.h" #include "../tnl/tnlBitStream.h" #include "../tnl/tnlNetConnection.h" #include "../tnl/tnlRandom.h" #include "../tnl/tnlSymmetricCipher.h" #include "../tnl/tnlAsymmetricKey.h" namespace TNLTest { TestGame *clientGame = NULL; TestGame *serverGame = NULL; //--------------------------------------------------------------------------------- TNL_IMPLEMENT_NETOBJECT(Player); // Player constructor Player::Player(Player::PlayerType pt) { // assign a random starting position for the player. startPos.x = TNL::Random::readF(); startPos.y = TNL::Random::readF(); endPos.x = startPos.x; endPos.y = startPos.y; renderPos = startPos; // preset the movement parameter for the player to the end of // the path t = 1.0; tDelta = 0; myPlayerType = pt; mNetFlags.set(Ghostable); game = NULL; } Player::~Player() { if(!game) return; // remove the player from the list of players in the game. for( TNL::S32 i = 0; i < game->players.size(); i++) { if(game->players[i] == this) { game->players.erase_fast(i); return; } } } void Player::addToGame(TestGame *theGame) { // add the player to the list of players in the game. theGame->players.push_back(this); game = theGame; if(myPlayerType == PlayerTypeMyClient) { // set the client player for the game for drawing the // scoping radius circle. game->clientPlayer = this; } } bool Player::onGhostAdd(TNL::GhostConnection *theConnection) { addToGame(((TestNetInterface *) theConnection->getInterface())->game); return true; } void Player::performScopeQuery(TNL::GhostConnection *connection) { // find all the objects that are "in scope" - for the purposes // of this test program, all buildings are considered to be in // scope always, as well as all "players" in a circle of radius // 0.25 around the scope object, or a radius squared of 0.0625 for(TNL::S32 i = 0; i < game->buildings.size(); i++) connection->objectInScope(game->buildings[i]); for(TNL::S32 i = 0; i < game->players.size(); i++) { Position playerP = game->players[i]->renderPos; TNL::F32 dx = playerP.x - renderPos.x; TNL::F32 dy = playerP.y - renderPos.y; TNL::F32 distSquared = dx * dx + dy * dy; if(distSquared < 0.0625) connection->objectInScope(game->players[i]); } } TNL::U32 Player::packUpdate(TNL::GhostConnection *connection, TNL::U32 updateMask, TNL::BitStream *stream) { // if this is the initial update, write out some static // information about this player. Since we never call // setMaskBits(InitialMask), we're guaranteed that this state // will only be updated on the first update for this object. if(stream->writeFlag(updateMask & InitialMask)) { // write one bit if it's not an AI if(stream->writeFlag(myPlayerType != PlayerTypeAI)) { // write a flag to the client if this is the player that // that client controls. stream->writeFlag(connection->getScopeObject() == this); } } // see if we need to write out the position data // we must write a bit to let the unpackUpdate know whether to // read the position data out of the stream. if(stream->writeFlag(updateMask & PositionMask)) { // since the position data is all 0 to 1, we can use // the bit stream's writeFloat method to write it out. // 12 bits should be sufficient precision stream->writeFloat(startPos.x, 12); stream->writeFloat(startPos.y, 12); stream->writeFloat(endPos.x, 12); stream->writeFloat(endPos.y, 12); stream->write(t); // fully precise t and tDelta stream->write(tDelta); } // if I had other states on the player that changed independently // of position, I could test for and update them here. // ... // the return value from packUpdate is the update mask for this // object on this client _AFTER_ this update has been sent. // Normally this will be zero, but it could be nonzero if this // object has a state that depends on some other object that // hasn't yet been ghosted. return 0; } void Player::unpackUpdate(TNL::GhostConnection *connection, TNL::BitStream *stream) { // see if the initial packet data was written: if(stream->readFlag()) { // check to see if it's not an AI player. if(stream->readFlag()) { if(stream->readFlag()) myPlayerType = PlayerTypeMyClient; else myPlayerType = PlayerTypeClient; } else myPlayerType = PlayerTypeAIDummy; } // see if the player's position has been updated: if(stream->readFlag()) { startPos.x = stream->readFloat(12); startPos.y = stream->readFloat(12); endPos.x = stream->readFloat(12); endPos.y = stream->readFloat(12); stream->read(&t); stream->read(&tDelta); // update the render position update(0); } } void Player::serverSetPosition(Position inStartPos, Position inEndPos, TNL::F32 inT, TNL::F32 inTDelta) { // update the instance variables of the object startPos = inStartPos; endPos = inEndPos; t = inT; tDelta = inTDelta; // notify the network system that the position state of this object has changed: setMaskBits(PositionMask); // call a quick RPC to all the connections that have this object in scope rpcPlayerDidMove(inEndPos.x, inEndPos.y); } void Player::update(TNL::F32 timeDelta) { t += tDelta * timeDelta; if(t >= 1.0) { t = 1.0; tDelta = 0; renderPos = endPos; // if this is an AI player on the server, if(myPlayerType == PlayerTypeAI) { startPos = renderPos; t = 0; endPos.x = TNL::Random::readF(); endPos.y = TNL::Random::readF(); tDelta = 0.2f + TNL::Random::readF() * 0.1f; setMaskBits(PositionMask); // notify the network system that the network state has been updated } } renderPos.x = startPos.x + (endPos.x - startPos.x) * t; renderPos.y = startPos.y + (endPos.y - startPos.y) * t; } void Player::onGhostAvailable(TNL::GhostConnection *theConnection) { // this function is called every time a ghost of this object is known // to be available on a given client. // we'll use this to demonstrate targeting a NetObject RPC // to a specific connection. Normally a RPC method marked as // RPCToGhost will be broadcast to ALL ghosts of the object currently in // scope. // first we construct an event that will represent the RPC call... // the RPC macros create a RPCNAME_construct function that returns // a NetEvent that encapsulates the RPC. TNL::NetEvent *theRPCEvent = TNL_RPC_CONSTRUCT_NETEVENT(this, rpcPlayerIsInScope, (renderPos.x, renderPos.y)); // then we can just post the event to whatever connections we want to have // receive the message. // In the case of NetObject RPCs, if the source object is not ghosted, // the RPC will just be silently dropped. theConnection->postNetEvent(theRPCEvent); } /* class RPCEV_Player_rpcPlayerIsInScope : public TNL::NetObjectRPCEvent { public: TNL::FunctorDecl x, TNL::Float<6> y)> mFunctorDecl; RPCEV_Player_rpcPlayerIsInScope() : mFunctorDecl(Player::rpcPlayerIsInScope_remote), RPCEvent(TNL::RPCGuaranteedOrdered, TNL::RPCToGhost) { mFunctor = &mFunctorDecl; } static TNL::NetClassRepInstance dynClassRep; virtual TNL::NetClassRep* getClassRep() const; bool checkClassType(TNL::Object *theObject) { return dynamic_cast(theObject) != __null; } }; TNL::NetClassRep* RPCEV_Player_rpcPlayerIsInScope::getClassRep() const { return &RPCEV_Player_rpcPlayerIsInScope::dynClassRep; } TNL::NetClassRepInstance RPCEV_Player_rpcPlayerIsInScope::dynClassRep("RPCEV_Player_rpcPlayerIsInScope",TNL::NetClassGroupGameMask, TNL::NetClassTypeEvent, 0); void Player::rpcPlayerIsInScope (TNL::Float<6> x, TNL::Float<6> y) { RPCEV_Player_rpcPlayerIsInScope *theEvent = new RPCEV_Player_rpcPlayerIsInScope; theEvent->mFunctorDecl.set (x, y) ; postNetEvent(theEvent); } TNL::NetEvent * Player::rpcPlayerIsInScope_construct (TNL::Float<6> x, TNL::Float<6> y) { RPCEV_Player_rpcPlayerIsInScope *theEvent = new RPCEV_Player_rpcPlayerIsInScope; theEvent->mFunctorDecl.set (x, y) ; return theEvent; } void Player::rpcPlayerIsInScope_test (TNL::Float<6> x, TNL::Float<6> y) { RPCEV_Player_rpcPlayerIsInScope *theEvent = new RPCEV_Player_rpcPlayerIsInScope; theEvent->mFunctorDecl.set (x, y) ; TNL::PacketStream ps; theEvent->pack(this, &ps); ps.setBytePosition(0); theEvent->unpack(this, &ps); theEvent->process(this); } void Player::rpcPlayerIsInScope_remote (TNL::Float<6> x, TNL::Float<6> y) { TNL::F32 fx = x, fy = y; TNL::logprintf("A player is now in scope at %g, %g", fx, fy); } */ TNL_IMPLEMENT_NETOBJECT_RPC(Player, rpcPlayerIsInScope, (TNL::Float<6> x, TNL::Float<6> y), (x, y), TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCToGhost, 0) { TNL::F32 fx = x, fy = y; TNL::logprintf("A player is now in scope at %g, %g", fx, fy); } TNL_IMPLEMENT_NETOBJECT_RPC(Player, rpcPlayerWillMove, (TNL::StringPtr testString), (testString), TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCToGhostParent, 0) { TNL::logprintf("Expecting a player move from the connection: %s", testString); } TNL_IMPLEMENT_NETOBJECT_RPC(Player, rpcPlayerDidMove, (TNL::Float<6> x, TNL::Float<6> y), (x, y), TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCToGhost, 0) { TNL::F32 fx = x, fy = y; TNL::logprintf("A player moved to %g, %g", fx, fy); } //--------------------------------------------------------------------------------- TNL_IMPLEMENT_NETOBJECT(Building); Building::Building() { // place the "building" in a random position on the screen upperLeft.x = TNL::Random::readF(); upperLeft.y = TNL::Random::readF(); lowerRight.x = upperLeft.x + TNL::Random::readF() * 0.1f + 0.025f; lowerRight.y = upperLeft.y + TNL::Random::readF() * 0.1f + 0.025f; game = NULL; // always scope the buildings to the clients mNetFlags.set(Ghostable); } Building::~Building() { if(!game) return; // remove the building from the list of buildings in the game. for( TNL::S32 i = 0; i < game->buildings.size(); i++) { if(game->buildings[i] == this) { game->buildings.erase_fast(i); return; } } } void Building::addToGame(TestGame *theGame) { // add it to the list of buildings in the game theGame->buildings.push_back(this); game = theGame; } bool Building::onGhostAdd(TNL::GhostConnection *theConnection) { addToGame(((TestNetInterface *) theConnection->getInterface())->game); return true; } TNL::U32 Building::packUpdate(TNL::GhostConnection *connection, TNL::U32 updateMask, TNL::BitStream *stream) { if(stream->writeFlag(updateMask & InitialMask)) { // we know all the positions are 0 to 1. // since this object is scope always, we don't care quite as much // how efficient the initial state updating is, since we're only // ever going to send this data when the client first connects // to this mission. stream->write(upperLeft.x); stream->write(upperLeft.y); stream->write(lowerRight.x); stream->write(lowerRight.y); } // for later - add a "color" field to the buildings that can change // when AIs or players move over them. return 0; } void Building::unpackUpdate(TNL::GhostConnection *connection, TNL::BitStream *stream) { if(stream->readFlag()) { stream->read(&upperLeft.x); stream->read(&upperLeft.y); stream->read(&lowerRight.x); stream->read(&lowerRight.y); } } //--------------------------------------------------------------------------------- TNL_IMPLEMENT_NETCONNECTION(TestConnection, TNL::NetClassGroupGame, true); TNL_IMPLEMENT_RPC(TestConnection, rpcGotPlayerPos, (bool b1, bool b2, TNL::StringTableEntry string, TNL::Float x, TNL::Float y), (b1, b2, string, x, y), TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCDirAny, 0) { TNL::F32 xv = x, yv = y; TNL::logprintf("Server acknowledged position update - %d %d %s %g %g", b1, b2, string.getString(), xv, yv); } TNL_IMPLEMENT_RPC(TestConnection, rpcSetPlayerPos, (TNL::F32 x, TNL::F32 y), (x, y), TNL::NetClassGroupGameMask, TNL::RPCGuaranteedOrdered, TNL::RPCDirClientToServer, 0) { Position newPosition; newPosition.x = x; newPosition.y = y; TNL::logprintf("%s - received new position (%g, %g) from client", getNetAddressString(), newPosition.x, newPosition.y); myPlayer->serverSetPosition(myPlayer->renderPos, newPosition, 0, 0.2f); // send an RPC back the other way! TNL::StringTableEntry helloString("Hello World!!"); rpcGotPlayerPos(true, false, helloString, x, y); }; TestConnection::TestConnection() { // every connection class instance must have a net class group set // before it is used. //setIsAdaptive(); // <-- Uncomment me if you want to use adaptive rate instead of fixed rate... } bool TestConnection::isDataToTransmit() { // we always want packets to be sent. return true; } void TestConnection::onConnectTerminated(NetConnection::TerminationReason reason, const char *string) { ((TestNetInterface *) getInterface())->pingingServers = true; } void TestConnection::onConnectionTerminated(NetConnection::TerminationReason reason, const char *edString) { logprintf("%s - %s connection terminated - reason %d.", getNetAddressString(), isConnectionToServer() ? "server" : "client", reason); if(isConnectionToServer()) ((TestNetInterface *) getInterface())->pingingServers = true; else delete (Player*)myPlayer; } void TestConnection::onConnectionEstablished() { // call the parent's onConnectionEstablished. // by default this will set the initiator to be a connection // to "server" and the non-initiator to be a connection to "client" Parent::onConnectionEstablished(); // To see how this program performs with 50% packet loss, // Try uncommenting the next line :) //setSimulatedNetParams(0.5, 0); if(isInitiator()) { setGhostFrom(false); setGhostTo(true); TNL::logprintf("%s - connected to server.", getNetAddressString()); ((TestNetInterface *) getInterface())->connectionToServer = this; } else { // on the server, we create a player object that will be the scope object // for this client. Player *player = new Player; myPlayer = player; myPlayer->addToGame(((TestNetInterface *) getInterface())->game); setScopeObject(myPlayer); setGhostFrom(true); setGhostTo(false); activateGhosting(); TNL::logprintf("%s - client connected.", getNetAddressString()); } } //--------------------------------------------------------------------------------- TestNetInterface::TestNetInterface(TestGame *theGame, bool server, const TNL::Address &bindAddress, const TNL::Address &pingAddr) : NetInterface(bindAddress) { game = theGame; isServer = server; pingingServers = !server; lastPingTime = 0; pingAddress = pingAddr; } void TestNetInterface::sendPing() { TNL::PacketStream writeStream; // the ping packet right now just has one byte for packet type writeStream.write(TNL::U8(GamePingRequest)); writeStream.sendto(mSocket, pingAddress); TNL::logprintf("%s - sending ping.", pingAddress.toString()); } void TestNetInterface::tick() { TNL::U32 currentTime = TNL::Platform::getRealMilliseconds(); if(pingingServers && (lastPingTime + PingDelayTime < currentTime)) { lastPingTime = currentTime; sendPing(); } checkIncomingPackets(); processConnections(); } // handleInfoPacket for the test game is very simple - if this instance // is a client, it pings for servers until one is found to connect to. // If this instance is a server, it responds to ping packets from clients. // More complicated games could maintain server lists, request game information // and more. void TestNetInterface::handleInfoPacket(const TNL::Address &address, TNL::U8 packetType, TNL::BitStream *stream) { TNL::PacketStream writeStream; if(packetType == GamePingRequest && isServer) { TNL::logprintf("%s - received ping.", address.toString()); // we're a server, and we got a ping packet from a client, // so send back a GamePingResponse to let the client know it // has found a server. writeStream.write(TNL::U8(GamePingResponse)); writeStream.sendto(mSocket, address); TNL::logprintf("%s - sending ping response.", address.toString()); } else if(packetType == GamePingResponse && pingingServers) { // we were pinging servers and we got a response. Stop the server // pinging, and try to connect to the server. TNL::logprintf("%s - received ping response.", address.toString()); TestConnection *connection = new TestConnection; connection->connect(this, address); // connect to the server through the game's network interface TNL::logprintf("Connecting to server: %s", address.toString()); pingingServers = false; } } //--------------------------------------------------------------------------------- //--------------------------------------------------------------------------------- TestGame::TestGame(bool server, const TNL::Address &interfaceBindAddr, const TNL::Address &pingAddress) { isServer = server; myNetInterface = new TestNetInterface(this, isServer, interfaceBindAddr, pingAddress); TNL::AsymmetricKey *theKey = new TNL::AsymmetricKey(32); myNetInterface->setPrivateKey(theKey); myNetInterface->setRequiresKeyExchange(true); lastTime = TNL::Platform::getRealMilliseconds(); if(isServer) { // generate some buildings and AIs: for(TNL::S32 i = 0; i < 50; i ++) { Building *building = new Building; building->addToGame(this); } for(TNL::S32 i = 0; i < 15; i ++) { Player *aiPlayer = new Player(Player::PlayerTypeAI); aiPlayer->addToGame(this); } serverPlayer = new Player(Player::PlayerTypeMyClient); serverPlayer->addToGame(this); } TNL::logprintf("Created a %s...", (server ? "server" : "client")); } TestGame::~TestGame() { delete myNetInterface; for(TNL::S32 i = 0; i < buildings.size(); i++) delete buildings[i]; for(TNL::S32 i = 0; i < players.size(); i++) delete players[i]; TNL::logprintf("Destroyed a %s...", (this->isServer ? "server" : "client")); } void TestGame::createLocalConnection(TestGame *serverGame) { myNetInterface->pingingServers = false; TestConnection *theConnection = new TestConnection; theConnection->connectLocal(myNetInterface, serverGame->myNetInterface); } void TestGame::tick() { TNL::U32 currentTime = TNL::Platform::getRealMilliseconds(); if(currentTime == lastTime) return; TNL::F32 timeDelta = (currentTime - lastTime) / 1000.0f; for(TNL::S32 i = 0; i < players.size(); i++) players[i]->update(timeDelta); myNetInterface->tick(); lastTime = currentTime; } void TestGame::moveMyPlayerTo(Position newPosition) { if(isServer) { serverPlayer->serverSetPosition(serverPlayer->renderPos, newPosition, 0, 0.2f); } else if(!myNetInterface->connectionToServer.isNull()) { TNL::logprintf("posting new position (%g, %g) to server", newPosition.x, newPosition.y); if(!clientPlayer.isNull()) clientPlayer->rpcPlayerWillMove("Whee! Foo!"); myNetInterface->connectionToServer->rpcSetPlayerPos(newPosition.x, newPosition.y); } } };