Tutorials/simplegame

From CubeiaWiki

Jump to: navigation, search

Contents

Tutorial - Creating a Simple Game on Firebase

We will create a very simple game as a tutorial on how to get started with game development on the Firebase platform. The game will essentially be a chat relay.The game will consist of two parts, the server and the client.

  • The game is what is deployed on a Firebase server
  • The client will connect to the Firebase server and the game

Prerequisites

This tutorial will use Maven for building. In short, you should have:

  • Java6 or higher
  • Maven 2.2.1 or higher
  • A Java IDE of your choice

This toturial will use Linux style commands when necessary, but it should be equally simple to perform on almost any system, Windows included.

The Server Game

Creating a Maven Project

We will start with creating a simple maven project from the Firebase archetypes. In you command line, type the following:

mvn archetype:generate -DarchetypeGroupId=com.cubeia.firebase.tools -DarchetypeArtifactId=firebase-game-archetype -DarchetypeVersion=1.8.0 -DarchetypeRepository=http://m2.cubeia.com/nexus/content/groups/public

This will create a new project for you with all dependencies and files created. It will ask for the following when creating the project:

  • "groupId" - This is the maven build group id of the new artifact, for example "com.mycompany.games"
  • "artifactId" - The maven build artifact id, for example "myBrilliantGame"
  • "version" - Maven build version, defaults to 1.0-SNAPSHOT
  • "package" - Package for Java or Flex builds, for example "com.mycompany.games"
  • "gameId" - The integer ID of the server game, asked for by the Flex and the Game archetypes, for example 1

The above should be standard if you know maven (except for "gameId"), otherwise just use our suggestions above to get on with it. Please note that the "gameId" should be an integer.

You will now have a new Maven project in a sub-folder named after the "artifactId" you supplied above. The project should have a POM looking something like this:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.games</groupId>
  <artifactId>myBrilliantGame</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>firebase-gar</packaging>
  <name>myBrilliantGame (game)</name>
  
  <dependencies>
    <dependency>
      <groupId>com.cubeia.firebase</groupId>
      <artifactId>firebase-api</artifactId>
      <version>1.8.0-CE</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.cubeia.tools</groupId>
        <artifactId>archive-plugin</artifactId>
        <version>1.10</version>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <groupId>com.cubeia.tools</groupId>
        <artifactId>firebase-maven-plugin</artifactId>
        <version>1.8.0-CE</version>
        <configuration>
          <deleteOnExit>false</deleteOnExit>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <repositories>
    <repository>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
      <id>cubeia-nexus</id>
      <name>Cubeia Repo</name>
      <url>http://m2.cubeia.com/nexus/content/groups/public</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
      <id>cubeia-nexus</id>
      <name>Cubeia Repo</name>
      <url>http://m2.cubeia.com/nexus/content/groups/public</url>
    </pluginRepository>
  </pluginRepositories>
</project>

The archetype has created a few files for you, two Java files we will be editing below and one deployment descriptor in 'src/main/resources/firebase/GAME-INF' which looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<game-definition id="1">
	<name>myBrilliantGame</name>
	<version>v0.0.1</version>
	<classname>com.mycompany.game.GameImpl</classname>
</game-definition>

As you can see from the descriptor above, it will use information from when you created the project. This file will be packaged with you Java code and picked up by Firebase when you deploy. If you change the name or package of the Game implementation, you should update the deployment descriptor "classname" element.

The Server Class

We will first create the server side of the game. The archetype above have already created a stub class for you, so go ahead and improt the Maven project into you favourite Java IDE. You should find two classes when done, "GameImpl" and "Processor".

The "GameImpl" class looks something like this:

package com.mycompany.games;

import com.cubeia.firebase.api.game.Game;
import com.cubeia.firebase.api.game.GameProcessor;
import com.cubeia.firebase.api.game.context.GameContext;

public class GameImpl implements Game {
	
	public void init(GameContext con) { }

	public GameProcessor getGameProcessor() {
		return new Processor(this);
	}
	
	public void destroy() { }
    
}

That is basically all the server side code you need. Of course, the game doesn't actually do anything yet.

You can now use Maven to compile the code and package the game archive for you. Execute the following command in the project root:

mvn package

If your project is compiling OK, you should now have a file called "myBrilliantGame-1.0-SNAPSHOT.gar" in you target directory.

Starting the Server

So, we're compiled OK, then let's run this little game in Firebase shall we? In order to keep it simple we'll let Maven handle that as well. In your project root, execute the following:

mvn firebase:run

This will run you newly created game on Firebase instance started from the command line. Smooth eh?

The Client

Now we'll create a very simple text based client so we can actually connect to our game. We'll extend a Firebase utility called, appropriately enough, "SimpleTextClient".

Creating a Maven Project

Again we'll use Maven. Open a new command line (in a new location probably) and execute the following:

mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.mycompany.client -DartifactId=myBrilliantClient

You probably recognize this as the standard way of creating a new Maven project for an ordinary JAR. It will create a new project for you. However, you'll need to add a dependency for the simple text client we will extend manually. You need:

<dependency>
  <groupId>com.cubeia.firebase.client</groupId>
  <artifactId>firebase-java-textclient</artifactId>
  <version>1.8.0-CE</version>
</dependency>

You also are going to need the Cubiea repository in your POM, so please add:

<repositories>
  <repository>
    <releases>
      <enabled>true</enabled>
    </releases>
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
    <id>cubeia-nexus</id>
    <name>Cubeia Repo</name>
    <url>http://m2.cubeia.com/nexus/content/groups/public</url>
  </repository>
</repositories>

Also, when importing your project into an editor, chances are you'll get source code verion 1.4 or some such. In that case, also add:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <source>1.6</source>
        <target>1.6</target>
      </configuration>
    </plugin>
  </plugins>
</build>

Which will make your entire POM look something like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.client</groupId>
  <artifactId>myBrilliantClient</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>myBrilliantClient</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.cubeia.firebase.client</groupId>
      <artifactId>firebase-java-textclient</artifactId>
      <version>1.8.0-CE</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <repositories>
    <repository>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
      <id>cubeia-nexus</id>
      <name>Cubeia Repo</name>
      <url>http://m2.cubeia.com/nexus/content/groups/public</url>
    </repository>
  </repositories>
</project>

The Client

As you now have a new Maven project, go ahead and import it into you Java IDE and let's create a simple text-based client.

We will extend "SimleTextClient" and implement some methods and constructors. We'll also rename the created "App" class to "TextClient".

package com.mycompany.client;

import java.util.regex.Pattern;

import com.cubeia.firebase.clients.java.connector.text.SimpleTextClient;

public class TextClient extends SimpleTextClient {

	public TextClient(String host, int port) {
		super(host, port);
	}

	public static void main(String[] args) {
	    if (args.length < 1) {
	    	System.err.println("Usage: java TextClient [port] host \nEx.: " +
            		"\n\t java TextClient localhost" +
            		"\n\t java TextClient 4123 localhost");
            return;
        }
		
        int hostIndex = 0;
        int port = 4123; // Default
        
        // If the first argument is a string of digits then we take that
        // to be the port number to use
        if (Pattern.matches("[0-9]+", args[0])) {
            port = Integer.parseInt(args[0]);            
            hostIndex = 1;
        }
       
        TextClient client = new TextClient(args[hostIndex], port);
        client.run();
	
	}
}

Testing the Connection

You can run the TextClient directly from your IDE. If you execute the class with no arguments you get:

Usage: java TextClient [port] host
Ex.:
         java TextClient localhost
         java TextClient 4123 localhost

Add the argument 'localhost' to the execution. If Firebase is up and running you should connect and (hopefully) see 'Protocol version verified'. If you see 'Protocol version mismatch', then your Firebase is running another IO-version then your client. If you set Firebase logging to debug as above you should see 'Client connected...' in the console window.

You connected to a Firebase server! Congratulations! Below follows a few simple commands you can try immediately.

Assuming you got version verified, type list 1 (where '1' is the "gameId" specified when you created your server game) and hit enter in the client (you should be able to type input in the console window). You should now get a list of tables available for the game. If you have not changed login handler in Firebase you can type login myself 123 and after logged in type join 1 2 which will seat you on table 1 seat 2.

You can open another client, login with a different password (in other words, do not use 'login myself 123' but rather something like 'login other 321'), and type watch 1. You should see myself sitting there. Try and join the table on a different seat with the second client, join 1 1. You should get a 'player joined' notice in the first client.

You can leave the table with leave 1 and logout with logout.

For a complete listing of commands type help. Note that most commands will require that you are logged in.

Game Communication

This far we have the game defined in the lobby, but we have no means of communicating between the client and the game logic. Let's change that.

Server

We're going to use a very simple way of communicating, we're going to use strings. Now, using string as means of communicating is not recommended for production code. We only use it to provide a simple tutorial.

Open up you IDE and the server game "Processor" class, the responsibility of this class is to handle incoming actions, from clients or other actors.

Let's start with adding a log entry for incoming actions, then send it to all clients at the table (like a chat server). That way we have proved the communication channel. Like so:

[...]
	
@Override
public void handle(GameDataAction action, Table table) {
    String message = new String(action.getData().array());
    System.out.println("Incoming message: " + message);
    table.getNotifier().notifyAllPlayers(action);
}

[...]

We will get what the client send wrapped in a ByteBuffer, since we know we are only sending strings we create a string from the underlying byte-data.

Client

For the client we need to override a method from the extended TextClient. The method HandleCommand will be called upon when no common command was intercepted by the gamecommon handler (e.g. join table, login etc.). For the game actions parameters, we need to know what table we should send to and the string to send. In accordance with the simple text cleint we use space as a delimiter.

Commands will be sent in using the following format: <tableid> <command>

In TextClient:

@Override
public void handleCommand(String command) {
    try {
       String[] args = command.split(" ");

       if (args.length != 2) {
           reportBadCommand("size="+args.length+", should be 2");
           return;
       }
		
       int tableid = Integer.parseInt(args[0]);
       ByteBuffer buf = ByteBuffer.wrap(args[1].getBytes());
			
       // Sends data wrapped in a GameTransportPacket
       // the context attribute is supplied by the super class
       context.getConnector().sendDataPacket(tableid, context.getPlayerId(), buf);
			
    } catch (Exception e) {
       reportBadCommand(e.toString());
    }
}

private void reportBadCommand(String error) {
    System.err.println("Invalid command ("+error+") Format: TID cmd");
}

The "context" is provided by the extended SimpleTextClient and holds IO specific objects such as the "connector" which can be used for sending data. The connector takes the game data as a byte buffer so we convert the command string to a byte buffer before sending.

Test Client -> Server Communication

Build and the run the game. Connect and login ('login myname 123') with a client. Join a table ('join 1 1') and you're ready to send an arbitrary command to the table, for example 1 Hello. This will cause "Hello" to be sent to your server game, and if you've found where the logging takes place, you should see this appearing:

Incoming command: Hello

Receiving Data in the Client

We now have data propagation from the client to the server, but we also need to handle data from the server to the client. We need to add a handler for the packets coming in the client. In the simple text client there is a class we can extend that provides interfaces for all client side packets which also allows us to plug it in to the filter chain of the "connector" (the connector is part of the context found in the SimpleTextClient).

Let's make a new class that extends AbstractClientPacketHandler:

package com.mycompany.client;

import com.cubeia.firebase.client.io.protocol.AbstractClientPacketHandler;

public class CommandHandler extends AbstractClientPacketHandler {
   
   ..... // Needed interface methods omitted, use auto-declaration
   
}

We are only interested in GameTransportPackets so we implement logic there to extract the string and print it to system out.

In CommandHandler:

@Override
public void visit(GameTransportPacket packet) {
    String cmd = new String(packet.gamedata);
    System.out.println("Message from ["+packet.pid+"] : "+cmd);
}

Now that we have our handler we must inject it to the filter chain of the connector. Change the constructor of TextClient:

public TextClient(String host, int port) {
    super(host, port);		
    // Add a game specific handler to the filter chain
    CommandHandler handler = new CommandHandler();
    addPacketHandler(handler);
}

Test Client <-> Server Communication

Build and run the game. Connect, login and seat two clients at the same table. Send a string to the table ('1 Hello'), both clients should receive a notification like:

Data from[11] : Hello

We now have fully working communication through a specific client and an implemented game. All that is left to do is implement a proper communication protocol and some logic for the game. =)

Wrap Up

That's it! We have now implemented no only a server game implementation, but a simple text client as well. In short we have:

  • Learned how to use Maven archetypes to create a new game.
  • Used Maven to build and run a game. For more Maven fun, have a look here.
  • Extended the SimpleTextClient to test with.
  • Implemented server and client logic.

Not bad! Now you should be ready to go on, good luck!

Personal tools