Basic Network Tutorial

From C4 Engine Wiki

Jump to: navigation, search

In this tutorial, we'll attempt to create a very basic chat application in the C4 engine. We'll set up a server, and have one or more clients connect to it. Next, whenever a client (or the server - the server is a client itself as well) 'says' something, that message will be sent to every other currently connected client.

This tutorial is composed of multiple parts. In part one, we'll set up the very basics of a chat system, use some built-in engine functionality for sending chats, etc. In part two, we'll add functionality for changing the user's names by sending customized Messages, add some validation for entered values (so that a user is limited into what he or she can send), and in part 3 we'll implement our own Say command, so we'll dive deeper into the Message system. In part 3, we'll also allow the users to use colors in their names and messages, to make it easier to discern people's messages.

First, you'll want to read the Networking Concepts article to get some basic information about C4's networking system, so you'll sorta know how the system works. Second, you'll want to read the Using the Command Console article - we'll only use console commands for this tutorial.

There's a few steps involved in this tutorial:

  • Set up a new C4 application project.
  • Create a header and a .cpp file for the chat program
  • Create two console command functions, a Server command, which starts the server, and a Join command, which initializes a connection to the given address.
  • Create a method that receives chats and displays them on-screen (in the console).

In C4, there's already a built-in chat command - the Say command. We won't be needing to add that one, lucky us.

For the full source code of this tutorial (part 1 + 2), see here.

Contents

Basic Chat Application

Anyway, let's begin. Start off by creating a new project under the C4 folder called Chat (or whatever you want to call it). If you don't know or can't remember how to create a new project, read the [Introduction - 'Hello World'] tutorial, which will teach you how to set up a new project.

In that project, create two new files by right-clicking the Chat project, then Add -> New Item. Create both a header and a .cpp file called Chat. In the header file, place the following code (read the comments to understand what its contents mean) :

// File Chat.h

#ifndef Chat_h
#define Chat_h

#include "C4Engine.h"
#include "C4Application.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 values define what protocol and what port to use - feel free to edit the game port if required.
	enum
	{
		kGameProtocol	= 0x0000000B,
		kGamePort	= 28327
	};

	class Chat : public Singleton<Chat>, public Application
	{

	private:
		// We first define two console commands.
		
		// The server command will start a new server
		Command serverCommand;
		// The join command will join an existing game.
		Command joinCommand;

	public:
		// Obligatory constructor / destructors.
		Chat();
		~Chat();

		// This method will be executed whenever the user uses the server command.
		static void ServerCommand(const char *params);

		// This method will be executed whenever the user uses the join command.
		static void JoinCommand(const char *params);

		// This method will be called by the engine whenever a chat is received.
		// It's used for a lot of other stuff, but that's outside the scope of this tutorial.
		void HandlePlayerEvent(PlayerEvent event, Player *player, const void *param);

	};

	// This is a pointer to the one instance of the Chat class through which
	// any other part of the application/game module can access it.
	extern Chat * TheChat;

}

#endif

Next, we create the C++ file, the implementation of our chat program's code. We'll do this in chunks. The first chunk is obligatory for all C4 applications, so they shouldn't be a problem if you've already done some C4 programs.

#include "Chat.h"

using namespace C4;

Chat *C4::TheChat = nullptr;

C4::Application *ConstructApplication(void)
{	
	return (new Chat);
}

Next, we add the constructor and destructor for our Chat application class. We'll initialize the commands in the constructor and register them with the engine, so that we define which command method should be executed with what console command. We'll want to set it up so that when the user types in 'server', the ServerCommand(const char *params) is executed, and when the user types in 'join <address>', the JoinCommand(const char *params) is executed.

The serverCommand and joinCommand Command instances defined in the header file are initialized with two arguments. The first argument is the console text the command should respond to, and the second argument is the method in your Chat class that should be executed. Note that both the Command variable and the command methods are named the same, where the method starts with a capital and the variable with a lower case letter.

Chat::Chat() :  Singleton<Chat>(TheChat),
		 // Create the server command to bind to the text 'server' in the console.
         	 serverCommand("server", &ServerCommand),
		
                // Create the join command to bind to the text 'join' in the console.
		joinCommand("join", &JoinCommand)
{
	// Register both console commands with the engine.
	TheEngine->AddCommand(&serverCommand);
	TheEngine->AddCommand(&joinCommand);

	// Set some settings in the network manager.
	TheNetworkMgr->SetProtocol(kGameProtocol);
	TheNetworkMgr->SetPortNumber(kGamePort);
}

// Our destructor is empty.
Chat::~Chat() {
}

Setting up a server

Excellent. Now, whenever we open the console (which we'll do once the program is done), we can type in 'server', and the ServerCommand(const char *params) will be executed. Similarily, whenever we type 'join', the JoinCommand(const char *params) method will be executed. There's a few things to do though, we'll have to write the actual commands. But they're not really that complicated, so here we go:

void Chat::ServerCommand(const char *params)
{
	// Just start the server. The 'true' parameter 
	// is to indicate that this machine is the server.
	TheMessageMgr->BeginMultiplayerGame(true);
        TheEngine->Report("Server initialized", kReportError);
}

See? Easy. Read the documentation about the last method here if you're interested in what goes on behind the scenes whenever you execute that command.

Joining a server

Next, we implement the JoinCommand method.

void Chat::JoinCommand(const char *params)
{
	// We'll first want to provide the user with some feedback - so he'll know what he's doing.
	String<128> str("Attempting to join ");
	str += params;
	TheEngine->Report(str, kReportError);

	// Next, we convert the entered parameters into a C4 NetworkAddress.
	// This format is used internally. It has both an IP address and a port number.
	NetworkAddress address = MessageMgr::StringToAddress(params);

	// We explicitly set a port in this example - it defaults to 0.
	address.SetPort(kGamePort);

	// Now we're just going to (try to) connect to the entered address.
	TheMessageMgr->Connect(address);
}

When you'll execute the join command, you'll see console messages appear on both the server and the client, indicating a connection has been established - or not. These messages come from our Chat class's superclass, the Application class. The next method we define - the HandlePlayerEvent - is also an override of the HandlePlayerEvent method in the Application class. We'll only write some to handle incoming chats, and leave the other possible player events to be handled by the Application's HandlePlayerEvent method.

Receiving a chat message

void Chat::HandlePlayerEvent(PlayerEvent event, Player *player, const void *param)
{
switch (event)
	{
		// We've received a chat. 
		case kPlayerChatReceived:
		{
			// We'll want to display the player's name in front of the chat message,
			// so we'll first paste the player's name and his message together in a String object.
			// We limit the size of the displayed text using the String class, which automatically
			// cuts off text that exceeds the boundary set in the template parameter.

			String<kMaxChatMessageLength + kMaxPlayerNameLength + 2> text(player->GetPlayerName());
			text += ": ";
			text += static_cast<const char *>(param);

			// Next, we'll make the completed message appear in the console.
			// The kReportError parameter tells the engine to put the message in the console. 
			// It doesn't actually mean there's an error.

			TheEngine->Report(text, kReportError);
			break;
		}
	}

	// Finally, we pass the player event to the parent Application's HandlePlayerEvent method,
	// so it can display errors if needed. The method does nothing at the moment, but we'll
	// add it just in case it will somewhere in the future.
	Application::HandlePlayerEvent(event, player, param);
}

That's all that's required code-wise. Next up is to compile and build the project into a .dll file that can be loaded into the C4 engine. Possible problems you may encounter during compiling are typing errors and the like. You'll want to make sure the ServerCommand and JoinCommand methods are defined as static in the header file - you can't pass a reference to an object-level method between methods, because the method is depending on an object instance, to say in short.


Our completed Chat.cpp file looks like this:

#include "Chat.h"

using namespace C4;

Chat *C4::TheChat = nullptr;

C4::Application *ConstructApplication(void)
{	
	return (new Chat);
}

Chat::Chat() : Singleton<Chat>(TheChat),
			   // Create the server command to bind to the text 'server' in the console.
			   serverCommand("server", &ServerCommand),

			   // Create the join command to bind to the text 'join' in the console.
			   joinCommand("join", &JoinCommand)
{
	// Register the console commands with the engine.
	TheEngine->AddCommand(&serverCommand);
	TheEngine->AddCommand(&joinCommand);

	// Set some settings in the network manager.
	TheNetworkMgr->SetProtocol(kGameProtocol);
	TheNetworkMgr->SetPortNumber(kGamePort);
}

Chat::~Chat() {
}

void Chat::ServerCommand(const char *params)
{
	// Next, we start the server. The 'true' parameter 
	// is to indicate that this machine is the server.
	TheMessageMgr->BeginMultiplayerGame(true);
	TheEngine->Report("Server initialized", kReportError);
}

void Chat::JoinCommand(const char *params)
{
	// We'll first want to provide the user with some feedback - so he'll know what he's doing.
	String<128> str("Attempting to join ");
	str += params;
	TheEngine->Report(str, kReportError);

	// Next, we convert the entered parameters into a C4 NetworkAddress.
	// This format is used internally. It has both an IP address and a port number.
	NetworkAddress address = MessageMgr::StringToAddress(params);

	// We explicitly set a port in this example - it defaults to 0.
	address.SetPort(kGamePort);

	// Now we're just going to (try to) connect to the entered address.
	TheMessageMgr->Connect(address);
}

void Chat::HandlePlayerEvent(PlayerEvent event, Player *player, const void *param)
{
switch (event)
	{
		// We've received a chat. 
		case kPlayerChatReceived:
		{
			// We'll want to display the player's name in front of the chat message,
			// so we'll first paste the player's name and his message together in a String object.
			// We limit the size of the displayed text using the String class, which automatically
			// cuts off text that exceeds the boundary set in the template parameter.
			String<kMaxChatMessageLength + kMaxPlayerNameLength + 2> text(player->GetPlayerName());
			text += ": ";
			text += static_cast<const char *>(param);

			// Next, we'll make the completed message appear in the console.
			// The kReportError parameter tells the engine to put the message in the console. 
			// It doesn't actually mean there's an error.
			TheEngine->Report(text, kReportError);
			break;
		}
	}

	// Finally, we pass the player event to the parent Application's HandlePlayerEvent method,
	// so it can display errors if needed. The method does nothing at the moment, but we'll
	// add it just in case it will somewhere in the future.
	Application::HandlePlayerEvent(event, player, param);
}


Usage

After building, edit the Variables.cfg file in your C4/Data/Engine folder, and change the $applicName value to "Chat". Make sure that, if you compiled, the Chat.dll file is in the main C4 directory, then, finally, launch C4.exe (or press Debug->Start Without Debugging in Visual Studio).

You'll want to test your program by opening the console window by pressing the tilde button (~) at the top left of your keyboard (to the left of the 1 button on most keyboard configurations), then type 'server', then press Enter. You should see a message appear saying 'Server initialized'.

Image:Network tutorial server init.JPG

Great. Next, you'll have to find a second computer or network connection to start sending messages to other people. This'll probably work best if you're on a LAN, but it should work anywhere basically. Start up C4.exe with the Chat application on both systems, and type 'server' on the system you'll want as a server. On that same system, you'll have to figure out its IP address, so you can tell the other (client) system to what it should connect. In Windows, press Start -> Run and type in 'cmd', then press Enter. This'll open up a command console In there, type 'ipconfig', then press enter. You'll see a list of information appear. Write down or remember the IP address listed in there and go to your client machine again. With C4 open, type in 'join <address>', where <address> is the address you just copied and you've just seen on the command console. Press Enter.

If everything went right, you should now have an 'Accept: <address>' message appear on your client, and an 'Open: <address>' message appear on your server. Note that you'll have to allow Windows Firewall to let C4 access the network / internet in order for it to work.

Image:Network tutorial server conn.JPG


Finally, you can type the command 'say <message>' on either the server or client machine, and you'll see that message appear on the other side.

Image:Network tutorial orly.JPG


You'll notice that both players will be called 'player' initially. In the next part of this tutorial, we'll add functionality to allow players to change their name in the chat window. Click here to go to part 2: Basic Network Tutorial - Part 2

Exercises

  • Update the HandlePlayerEvent method so that messages only appear if they come from other clients. You can use a comparison with TheMessageMgr->GetLocalPlayer() and the Player object passed to the HandlePlayerEvent() method.
  • Play around with colors. You can use the code [#000] anywhere in your text to alter the color of the following text to the value you enter in 000. Each of the three numbers represents the amount of red, green and blue you want to use in your color. You can use the values 0 to 9, A, B, C, D, E and F in there, or [#000] to [#FFF].

Full source code

For the full source code of this tutorial (part 1 + 2), see Chat Application.

Personal tools