About how we made the game for Google Play

About how we made Stickers for Google Play


For a long time I had the idea to share my knowledge with the community. At first I wanted to write something on astrophysics or GTR, but I decided nevertheless that it would be more correct to write about that subject area, which I deal with professionally. So, I will try to explain in detail the process of creating and the subtleties of the implementation of the game application for Android (from design to publication and In App purchases).


Introduction


I have been engaged in programming since the first class, I graduated from Applied Mathematics of St. Petersburg State Polytechnic University. Recently (about a year ago) I discovered development for mobile platforms. It became interesting what it is and what it is eaten with. I am currently developing several projects in a team of friends / colleagues, but I would like to write about my first experience. Such an experience was writing a game application - “ Stickers ” (Who Am I?).

What kind of stickers are these?
For those who are not in the know - I will explain. Stickers - this is such a table game in which each player gets a piece of paper with some famous character on their foreheads (characters are invented by each other playing). The goal of each participant is to guess the character he has guessed.
The gameplay is a sequential yes / no task of questions and receiving answers to them from other players.


The choice fell on Stickers for several reasons.
Firstly, we did not find any analogues in the market (this refers to the implementation of the rules of the described table game).
Secondly, I wanted to write something not very time-consuming.
Thirdly, the game is quite popular in our circles and we thought that maybe someone would be interested to play it and virtually.

Development process


Formulation of the problem

The task was formed quite unequivocal. It is necessary to implement a client-server application that allows its users to use the following features:
  • Create your own account
  • Authentication Using Your Own Account
  • View Player Rankings
  • Creating a game room
  • Entrance to the game room
  • Gameplay participation


The gameplay is a phase change:
  • Questions phase
  • Voting phase


UI Design

Fortunately, my wife is a designer and I practically didn’t have to take part in choosing a palette, arrangement of elements and other designer things. Based on the analysis of the capabilities that the game should provide to the player, it was decided how many game states (Activities) will be and what controls should be in each of them:
  • Main menu
    • Authentication
    • check in
    • Access to player rating

  • Player Rating
    • Return to the main menu.

  • Room List
    • Entrance to the room
    • Create your own room
    • Go to the main menu

  • Current room
    • Exit the room

  • Game state number 1: input question
    • Entering a Question
    • Transition to the history of questions
    • Go to answer input
    • Go to the main menu

  • Game state No. 2: view the history of questions
    • Go to question input
    • Go to answer input
    • Go to the main menu

  • Game state No. 3: input response
    • Response input
    • Go to question input
    • Transition to the history of questions
    • Go to the main menu

  • Game state No. 4: voting
    • Voice input
    • Go to the main menu

  • Victory
    • Go to the main menu

  • Defeat
    • Go to Wikipedia (to character page)
    • Go to the main menu



State transition diagram




DB Design

As soon as it became clear to us what game states and objects exist in the game, we proceeded to their formalization in terms of the database.

So, we need the following tables:
  • Users A table that stores information about all users
  • Games A table that stores information about all rooms
  • Characters. A table that stores information about all the characters
  • Questions. A table that stores user questions about characters he has made
  • Answers The table where user responses are stored


In the initial version of the game there were only these tables, but the game developed and new ones were added. I will not describe the rest of the tables, otherwise the narration will be excessively long. All tables are shown in the database diagram, but their presence will not hinder further discussion.

Database schema



The relationships between the tables changed several times (agile, so to speak), but in the end the following remained:
  • Each user can be associated with one character.
  • Each user can only be in one room.
  • Each question can only be asked by one user.
  • Each question can relate to only one character.
  • Each question can only be asked within one game.
  • Each answer can only be given by one user.
  • Each answer can only be given to one question.

And where is the normalization of data?
Duplicate communications are needed only to reduce the load on the DBMS and they appeared far from the first version of the game. With the increase in the number of tables, the number of aggregations that need to be done for certain data samples has increased.


Application level

Finally, we got to the software implementation. So, I will start with the most common words. The whole project consists of 4 modules:
  • Venta. A library that compiles useful utilities
  • Protocol. Library with a description of the interaction protocol
  • Server Application Server
  • Client Client side of the application


Project outline


Venta Library

Since I like to reinvent the wheel and don’t like the mishmash of third-party libraries (yes, yes, a classic problem of many pedant programmers), I decided to write some things myself. I have been writing this library for a long time and it contains many useful utilities for me (working with a database, client-server interaction, actors, mathematics, encryption ...).
In this article, I want to talk about the network part of this library. I decided to implement the interaction between the client and server by serializing / deserializing objects, among which there are requests and answers. The Message object is the elementary information unit being transferred (at the library level, of course) :

”Message.java”
package com.gesoftware.venta.network.model;
import com.gesoftware.venta.utility.CompressionUtility;
import java.nio.charset.Charset;
import java.io.Serializable;
import java.util.Arrays;
/* *
 * Message class definition
 * */
public final class Message implements Serializable {
    /* Time */
    private final long m_Timestamp;
    /* Message data */
    private final byte[] m_Data;
    /* *
     * METHOD: Message class constructor
     *  PARAM: [IN] data - bytes array data
     * AUTHOR: Eliseev Dmitry
     * */
    public Message(final byte data[]) {
        m_Timestamp = System.currentTimeMillis();
        m_Data      = data;
    } /* End of 'Message::Message' method */
    /* *
     * METHOD: Message class constructor
     *  PARAM: [IN] data - bytes array data
     * AUTHOR: Eliseev Dmitry
     * */
    public Message(final String data) {
        this(data.getBytes());
    } /* End of 'Message::Message' method */
    /* *
     * METHOD: Message class constructor
     *  PARAM: [IN] object - some serializable object
     * AUTHOR: Eliseev Dmitry
     * */
    public Message(final Object object) {
        this(CompressionUtility.compress(object));
    } /* End of 'Message::Message' method */
    /* *
     * METHOD: Bytes data representation getter
     * RETURN: Data bytes representation
     * AUTHOR: Eliseev Dmitry
     * */
    public final byte[] getData() {
        return m_Data;
    } /* End of 'Message::getData' method */
    /* *
     * METHOD: Gets message size
     * RETURN: Data size in bytes
     * AUTHOR: Eliseev Dmitry
     * */
    public final int getSize() {
        return (m_Data != null)?m_Data.length:0;
    } /* End of 'Message::getSize' method */
    @Override
    public final String toString() {
        return (m_Data != null)?new String(m_Data, Charset.forName("UTF-8")):null;
    } /* End of 'Message::toString' method */
    /* *
     * METHOD: Compares two messages sizes
     * RETURN: TRUE if messages has same sizes, FALSE otherwise
     *  PARAM: [IN] message - message to compare with this one
     * AUTHOR: Eliseev Dmitry
     * */
    private boolean messagesHasSameSizes(final Message message) {
        return m_Data != null && m_Data.length == message.m_Data.length;
    } /* End of 'Message::messagesHasSameSize' method */
    /* *
     * METHOD: Compares two messages by their values
     * RETURN: TRUE if messages has same sizes, FALSE otherwise
     *  PARAM: [IN] message - message to compare with this one
     * AUTHOR: Eliseev Dmitry
     * */
    private boolean messagesAreEqual(final Message message) {
        /* Messages has different sizes */
        if (!messagesHasSameSizes(message))
            return false;
        /* At least one of characters is not equal to same at another message */
        for (int i = 0; i < message.m_Data.length; i++)
            if (m_Data[i] != message.m_Data[i])
                return false;
        /* Messages are equal */
        return true;
    } /* End of 'Message::messagesAreEqual' method */
    /* *
     * METHOD: Tries to restore object, that may be packed in message
     * RETURN: Restored object if success, null otherwise
     * AUTHOR: Eliseev Dmitry
     * */
    public final Object getObject() {
        return CompressionUtility.decompress(m_Data);
    } /* End of 'Message::getObject' method */
    /* *
     * METHOD: Gets message sending time (in server time)
     * RETURN: Message sending time
     * AUTHOR: Eliseev Dmitry
     * */
    public final long getTimestamp() {
        return m_Timestamp;
    } /* End of 'Message::getTimestamp' method */
    @Override
    public final boolean equals(Object obj) {
        return obj instanceof Message && messagesAreEqual((Message) obj);
    } /* End of 'Message::equals' method */
    @Override
    public final int hashCode() {
        return Arrays.hashCode(m_Data);
    } /* End of 'Message::hashCode' method */
} /* End of 'Message' class */



I will not dwell on the description of this object, the code is quite commented.

Simplification of work with the network is due to the use of two classes:
  • Server (server side)
  • Connection (client side)


When creating an object of type Server , you must specify the port on which it will wait for incoming connections and the implementation of the IServerHandler interface

”IServerHandler.java”
package com.gesoftware.venta.network.handlers;
import com.gesoftware.venta.network.model.Message;
import com.gesoftware.venta.network.model.ServerResponse;
import java.net.InetAddress;
/* Server handler interface declaration */
public interface IServerHandler {
    /* *
     * METHOD: Will be called right after new client connected
     * RETURN: True if you accept connected client, false if reject
     *  PARAM: [IN] clientID      - client identifier (store it somewhere)
     *  PARAM: [IN] clientAddress - connected client information
     * AUTHOR: Eliseev Dmitry
     * */
    public abstract boolean onConnect(final String clientID, final InetAddress clientAddress);
    /* *
     * METHOD: Will be called right after server accept message from any connected client
     * RETURN: Response (see ServerResponse class), or null if you want to disconnect client
     *  PARAM: [IN] clientID - sender identifier
     *  PARAM: [IN] message  - received message
     * AUTHOR: Eliseev Dmitry
     * */
    public abstract ServerResponse onReceive(final String clientID, final Message message);
    /* *
     * METHOD: Will be called right after any client disconnected
     *  PARAM: [IN] clientID - disconnected client identifier
     * AUTHOR: Eliseev Dmitry
     * */
    public abstract void onDisconnect(final String clientID);
} /* End of 'IServerHandler' interface */



The client, in turn, when creating an object of type Connection, must provide an implementation of the IClientHandler interface .

”IClientHandler.java”
package com.gesoftware.venta.network.handlers;
import com.gesoftware.venta.network.model.Message;
import com.gesoftware.venta.network.model.ServerResponse;
import java.net.InetAddress;
/* Server handler interface declaration */
public interface IServerHandler {
    /* *
     * METHOD: Will be called right after new client connected
     * RETURN: True if you accept connected client, false if reject
     *  PARAM: [IN] clientID      - client identifier (store it somewhere)
     *  PARAM: [IN] clientAddress - connected client information
     * AUTHOR: Eliseev Dmitry
     * */
    public abstract boolean onConnect(final String clientID, final InetAddress clientAddress);
    /* *
     * METHOD: Will be called right after server accept message from any connected client
     * RETURN: Response (see ServerResponse class), or null if you want to disconnect client
     *  PARAM: [IN] clientID - sender identifier
     *  PARAM: [IN] message  - received message
     * AUTHOR: Eliseev Dmitry
     * */
    public abstract ServerResponse onReceive(final String clientID, final Message message);
    /* *
     * METHOD: Will be called right after any client disconnected
     *  PARAM: [IN] clientID - disconnected client identifier
     * AUTHOR: Eliseev Dmitry
     * */
    public abstract void onDisconnect(final String clientID);
} /* End of 'IServerHandler' interface */


Now a little about the internal structure of the server. As soon as the next client joins the server, a unique hash is calculated for it and two streams are created: the receive stream and the send stream. The receive stream is blocked and awaits a message from the client. As soon as the message from the client has been received, it is transmitted to the handler registered by the library user. As a result of processing, one of five events may occur:
  • Disconnecting a client (let's say a disconnect request has arrived)
  • Sending a response to a client
  • Sending a response to another client
  • Sending a response to all connected clients
  • Sending a response to a certain group of clients


If now it is necessary to send a message to one of the connected clients, it is placed in the message sending queue of this client, and the thread responsible for sending is notified that new messages have appeared in the queue.

Clearly, the data flow can be demonstrated by the diagram below.
Data flow in the network module of the library



Client X sends a request to the server (red arrow). The request is received in the receiver stream corresponding to the client. It immediately calls the message handler (yellow arrow). As a result of processing, a certain response is formed, which is placed in the sending queue of client X (green arrow). The send stream checks for messages in the send queue (black arrow) and sends a response to the client (blue arrow).

Example (multi-user echo server)
package com.gesoftware.venta.network;
import com.gesoftware.venta.logging.LoggingUtility;
import com.gesoftware.venta.network.handlers.IClientHandler;
import com.gesoftware.venta.network.handlers.IServerHandler;
import com.gesoftware.venta.network.model.Message;
import com.gesoftware.venta.network.model.ServerResponse;
import java.net.InetAddress;
import java.util.TimerTask;
public final class NetworkTest {
    private final static int c_Port = 5502;
    private static void startServer() {
        final Server server = new Server(c_Port, new IServerHandler() {
            @Override
            public boolean onConnect(final String clientID, final InetAddress clientAddress) {
                LoggingUtility.info("Client connected: " + clientID);
                return true;
            }
            @Override
            public ServerResponse onReceive(final String clientID, final Message message) {
                LoggingUtility.info("Client send message: " + message.toString());
                return new ServerResponse(message);
            }
            @Override
            public void onDisconnect(final String clientID) {
                LoggingUtility.info("Client disconnected: " + clientID);
            }
        });
        (new Thread(server)).start();
    }
    private static class Task extends TimerTask {
        private final Connection m_Connection;
        public Task(final Connection connection) {
            m_Connection = connection;
        }
        @Override
        public void run() {
            m_Connection.send(new Message("Hello, current time is: " + System.currentTimeMillis()));
        }
    }
    private static void startClient() {
        final Connection connection = new Connection("localhost", c_Port, new IClientHandler() {
            @Override
            public void onReceive(final Message message) {
                LoggingUtility.info("Server answer: " + message.toString());
            }
            @Override
            public void onConnectionLost(final String message) {
                LoggingUtility.info("Connection lost: " + message);
            }
        });
        connection.connect();
        (new java.util.Timer("Client")).schedule(new Task(connection), 0, 1000);
    }
    public static void main(final String args[]) {
        LoggingUtility.setLoggingLevel(LoggingUtility.LoggingLevel.LEVEL_DEBUG);
        startServer();
        startClient();
    }
}


Pretty short, isn't it?

Game server

The architecture of the game server is multilevel. Immediately I will give her scheme, and then a description.
Server architecture diagram


So, the connection pool is used to interact with the database (I use the BoneCP library). To work with prepared statements, I wrapped the connection in my own class (Venta library).

DBConnection.java
package com.gesoftware.venta.db;
import com.gesoftware.venta.logging.LoggingUtility;
import com.jolbox.bonecp.BoneCPConfig;
import com.jolbox.bonecp.BoneCP;
import java.io.InputStream;
import java.util.AbstractList;
import java.util.LinkedList;
import java.util.HashMap;
import java.util.Map;
import java.sql.*;
/**
 * DB connection class definition
 **/
public final class DBConnection {
    /* Connections pool */
    private BoneCP m_Pool;
    /**
     * DB Statement class definition
     **/
    public final class DBStatement {
        private final PreparedStatement m_Statement;
        private final Connection m_Connection;
        /* *
         * METHOD: Class constructor
         *  PARAM: [IN] connection - current connection
         *  PARAM: [IN] statement  - statement, created from connection
         * AUTHOR: Dmitry Eliseev
         * */
        private DBStatement(final Connection connection, final PreparedStatement statement) {
            m_Connection = connection;
            m_Statement  = statement;
        } /* End of 'DBStatement::DBStatement' class */
        /* *
         * METHOD: Integer parameter setter
         * RETURN: True if success, False otherwise
         *  PARAM: [IN] index - parameter position
         *  PARAM: [IN] value - parameter value
         * AUTHOR: Dmitry Eliseev
         * */
        public final boolean setInteger(final int index, final int value) {
            try {
                m_Statement.setInt(index, value);
                return true;
            } catch (final SQLException e) {
                LoggingUtility.debug("Can't set integer value: " + value + " because of " + e.getMessage());
            }
            return false;
        } /* End of 'DBStatement::setInteger' class */
        /* *
         * METHOD: Long parameter setter
         * RETURN: True if success, False otherwise
         *  PARAM: [IN] index - parameter position
         *  PARAM: [IN] value - parameter value
         * AUTHOR: Dmitry Eliseev
         * */
        public final boolean setLong(final int index, final long value) {
            try {
                m_Statement.setLong(index, value);
                return true;
            } catch (final SQLException e) {
                LoggingUtility.debug("Can't set long value: " + value + " because of " + e.getMessage());
            }
            return false;
        } /* End of 'DBStatement::setLong' class */
        /* *
         * METHOD: String parameter setter
         * RETURN: True if success, False otherwise
         *  PARAM: [IN] index - parameter position
         *  PARAM: [IN] value - parameter value
         * AUTHOR: Dmitry Eliseev
         * */
        public final boolean setString(final int index, final String value) {
            try {
                m_Statement.setString(index, value);
            } catch (final SQLException e) {
                LoggingUtility.debug("Can't set string value: " + value + " because of " + e.getMessage());
            }
            return false;
        } /* End of 'DBStatement::setString' class */
        /* *
         * METHOD: Enum parameter setter
         * RETURN: True if success, False otherwise
         *  PARAM: [IN] index - parameter position
         *  PARAM: [IN] value - parameter value
         * AUTHOR: Dmitry Eliseev
         * */
        public final boolean setEnum(final int index, final Enum value) {
            return setString(index, value.name());
        } /* End of 'DBStatement::setEnum' method */
        /* *
         * METHOD: Binary stream parameter setter
         * RETURN: True if success, False otherwise
         *  PARAM: [IN] index  - parameter position
         *  PARAM: [IN] stream - stream
         *  PARAM: [IN] long   - data length
         * AUTHOR: Dmitry Eliseev
         * */
        public final boolean setBinaryStream(final int index, final InputStream stream, final long length) {
            try {
                m_Statement.setBinaryStream(index, stream);
                return true;
            } catch (final SQLException e) {
                LoggingUtility.debug("Can't set stream value: " + stream + " because of " + e.getMessage());
            }
            return false;
        } /* End of 'DBStatement::setBinaryStream' method */
    } /* End of 'DBConnection::DBStatement' class */
    /* *
     * METHOD: Class constructor
     *  PARAM: [IN] host - Database service host
     *  PARAM: [IN] port - Database service port
     *  PARAM: [IN] name - Database name
     *  PARAM: [IN] user - Database user's name
     *  PARAM: [IN] pass - Database user's password
     * AUTHOR: Dmitry Eliseev
     * */
    public DBConnection(final String host, final int port, final String name, final String user, final String pass) {
        final BoneCPConfig config = new BoneCPConfig();
        config.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + name);
        config.setUsername(user);
        config.setPassword(pass);
        /* Pool size configuration */
        config.setMaxConnectionsPerPartition(5);
        config.setMinConnectionsPerPartition(5);
        config.setPartitionCount(1);
        try {
            m_Pool = new BoneCP(config);
        } catch (final SQLException e) {
            LoggingUtility.error("Can't initialize connections pool: " + e.getMessage());
            m_Pool = null;
        }
    } /* End of 'DBConnection::DBConnection' method */
    @Override
    protected final void finalize() throws Throwable {
        super.finalize();
        if (m_Pool != null)
            m_Pool.shutdown();
    } /* End of 'DBConnection::finalize' method  */
    /* *
     * METHOD: Prepares statement using current connection
     * RETURN: Prepared statement
     *  PARAM: [IN] query - SQL query
     * AUTHOR: Dmitry Eliseev
     * */
    public final DBStatement createStatement(final String query) {
        try {
            LoggingUtility.debug("Total: " + m_Pool.getTotalCreatedConnections() + "; Free: " + m_Pool.getTotalFree() + "; Leased: " + m_Pool.getTotalLeased());
            final Connection connection = m_Pool.getConnection();
            return new DBStatement(connection, connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS));
        } catch (final SQLException e) {
            LoggingUtility.error("Can't create prepared statement using query: " + e.getMessage());
        } catch (final Exception e) {
            LoggingUtility.error("Connection wasn't established: " + e.getMessage());
        }
        return null;
    } /* End of 'DBConnection::createStatement' method */
    /* *
     * METHOD: Closes prepared statement
     *  PARAM: [IN] sql - prepared statement
     * AUTHOR: Dmitry Eliseev
     * */
    private void closeStatement(final DBStatement query) {
        if (query == null)
            return;
        try {
            if (query.m_Statement != null)
                query.m_Statement.close();
            if (query.m_Connection != null)
                query.m_Connection.close();
        } catch (final SQLException ignored) {}
    } /* End of 'DBConnection::closeStatement' method */
    /* *
     * METHOD: Executes prepared statement like INSERT query
     * RETURN: Inserted item identifier if success, 0 otherwise
     *  PARAM: [IN] sql - prepared statement
     * AUTHOR: Dmitry Eliseev
     * */
    public final long insert(final DBStatement query) {
        try {
            /* Query execution */
            query.m_Statement.execute();
            /* Obtain last insert ID */
            final ResultSet resultSet = query.m_Statement.getGeneratedKeys();
            if (resultSet.next())
                return resultSet.getInt(1);
        } catch (final SQLException e) {
            LoggingUtility.error("Can't execute insert query: " + query.toString());
        } finally {
            closeStatement(query);
        }
        /* Insertion failed */
        return 0;
    } /* End of 'DBConnection::insert' method */
    /* *
     * METHOD: Executes prepared statement like UPDATE query
     * RETURN: True if success, False otherwise
     *  PARAM: [IN] sql - prepared statement
     * AUTHOR: Dmitry Eliseev
     * */
    public final boolean update(final DBStatement query) {
        try {
            query.m_Statement.execute();
            return true;
        } catch (final SQLException e) {
            LoggingUtility.error("Can't execute update query: " + query.m_Statement.toString());
        } finally {
            closeStatement(query);
        }
        /* Update failed */
        return false;
    } /* End of 'DBConnection::update' method */
    /* *
     * METHOD: Executes prepared statement like COUNT != 0 query
     * RETURN: True if exists, False otherwise
     *  PARAM: [IN] sql - prepared statement
     * AUTHOR: Dmitry Eliseev
     * */
    public final boolean exists(final DBStatement query) {
        final AbstractList> results = select(query);
        return results != null && results.size() != 0;
    } /* End of 'DBConnection::DBConnection' method */
    /* *
     * METHOD: Executes prepared statement like SELECT query
     * RETURN: List of records (maps) if success, null otherwise
     *  PARAM: [IN] sql - prepared statement
     * AUTHOR: Dmitry Eliseev
     * */
    public final AbstractList> select(final DBStatement query) {
        try {
            /* Container for result set */
            final AbstractList> results = new LinkedList>();
            /* Query execution */
            query.m_Statement.execute();
            /* Determine columns meta data */
            final ResultSetMetaData metaData = query.m_Statement.getMetaData();
            /* Obtain real data */
            final ResultSet resultSet = query.m_Statement.getResultSet();
            while (resultSet.next()) {
                final Map row = new HashMap();
                /* Copying fetched data */
                for (int columnID = 1; columnID <= metaData.getColumnCount(); columnID++)
                    row.put(metaData.getColumnName(columnID), resultSet.getObject(columnID));
                /* Add row to results */
                results.add(row);
            }
            /* That's it */
            return results;
        } catch (final SQLException e) {
            LoggingUtility.error("Can't execute select query: " + query.toString());
        } finally {
            closeStatement(query);
        }
        /* Return empty result */
        return null;
    } /* End of 'DBConnection::select' method */
} /* End of 'DBConnection' class */



You should also pay attention to the DBController.java class:
DBController.java
package com.gesoftware.venta.db;
import com.gesoftware.venta.logging.LoggingUtility;
import java.util.*;
/**
 * DB controller class definition
 **/
public abstract class DBController {
    /* Real DB connection */
    protected final DBConnection m_Connection;
    /* *
     * METHOD: Class constructor
     *  PARAM: [IN] connection - real DB connection
     * AUTHOR: Dmitry Eliseev
     * */
    protected DBController(final DBConnection connection) {
        m_Connection = connection;
        LoggingUtility.core(getClass().getCanonicalName() + " controller initialized");
    } /* End of 'DBController::DBController' method */
    /* *
     * METHOD: Requests collection of T objects using select statement
     * RETURN: Collection of objects if success, empty collection otherwise
     *  PARAM: [IN] selectStatement - prepared select statement
     * AUTHOR: Dmitry Eliseev
     * */
    protected final Collection getCollection(final DBConnection.DBStatement selectStatement) {
        if (selectStatement == null)
            return new LinkedList();
        final AbstractList> objectsCollection = m_Connection.select(selectStatement);
        if ((objectsCollection == null)||(objectsCollection.size() == 0))
            return new LinkedList();
        final Collection parsedObjectsCollection = new ArrayList(objectsCollection.size());
        for (final Map object : objectsCollection)
            parsedObjectsCollection.add(parse(object));
        return parsedObjectsCollection;
    } /* End of 'DBController::getCollection' method */
    /* *
     * METHOD: Requests one T object using select statement
     * RETURN: Object if success, null otherwise
     *  PARAM: [IN] selectStatement - prepared select statement
     * AUTHOR: Dmitry Eliseev
     * */
    protected final T getObject(final DBConnection.DBStatement selectStatement) {
        if (selectStatement == null)
            return null;
        final AbstractList> objectsCollection = m_Connection.select(selectStatement);
        if ((objectsCollection == null)||(objectsCollection.size() != 1))
            return null;
        return parse(objectsCollection.get(0));
    } /* End of 'DBController::getObject' method */
    /* *
     * METHOD: Parses object's map representation to real T object
     * RETURN: T object if success, null otherwise
     *  PARAM: [IN] objectMap - object map, obtained by selection from DB
     * AUTHOR: Dmitry Eliseev
     * */
    protected abstract T parse(final Map objectMap);
} /* End of 'DBController' class */



The DBController class is designed to work with objects of any particular table. In the server application, controllers are created for each of the database tables. At the controller level, methods for inserting, retrieving, updating data in the database are implemented.

Some operations require changing data in several tables at once. For this, a level of managers has been created. Each manager has access to all controllers. At the manager level, higher-level operations are implemented, for example, “Place user X in room A”. In addition to moving to a new level of abstraction, managers implement a data caching mechanism. For example, there is no need to go into the database whenever someone tries to authenticate or wants to know their rating. The managers responsible for users or user ratings store this data. Thus, the overall load on the database is reduced.

The next level of abstraction is handlers. The following class is used as an implementation of the IserverHandler interface:
StickersHandler.java
package com.gesoftware.stickers.server.handlers;
import com.gesoftware.stickers.model.common.Definitions;
public final class StickersHandler implements IServerHandler {
    private final Map m_Handlers = new SynchronizedMap();
    private final StickersManager m_Context;
    private final JobsManager m_JobsManager;
    public StickersHandler(final DBConnection connection) {
        m_Context     = new StickersManager(connection);
        m_JobsManager = new JobsManager(Definitions.c_TasksThreadSleepTime);
        registerQueriesHandlers();
        registerJobs();
    }
    private void registerJobs() {
        m_JobsManager.addTask(new TaskGameUpdateStatus(m_Context));
        m_JobsManager.addTask(new TaskGameUpdatePhase(m_Context));
    }
    private void registerQueriesHandlers() {
        /* Menu handlers */
        m_Handlers.put(QueryAuthorization.class, new QueryAuthorizationHandler(m_Context));
        m_Handlers.put(QueryRegistration.class,  new QueryRegistrationHandler(m_Context));
        m_Handlers.put(QueryRating.class,        new QueryRatingHandler(m_Context));
        /* Logout */
        m_Handlers.put(QueryLogout.class, new QueryLogoutHandler(m_Context));
        /* Rooms handlers */
        m_Handlers.put(QueryRoomRefreshList.class, new QueryRoomRefreshListHandler(m_Context));
        m_Handlers.put(QueryRoomCreate.class,      new QueryRoomCreateHandler(m_Context));
        m_Handlers.put(QueryRoomSelect.class,      new QueryRoomSelectHandler(m_Context));
        m_Handlers.put(QueryRoomLeave.class,       new QueryRoomLeaveHandler(m_Context));
        /* Games handler */
        m_Handlers.put(QueryGameLeave.class,       new QueryGameLeaveHandler(m_Context));
        m_Handlers.put(QueryGameIsStarted.class,   new QueryGameIsStartedHandler(m_Context));
        m_Handlers.put(QueryGameWhichPhase.class,  new QueryGameWhichPhaseHandler(m_Context));
        /* Question handler */
        m_Handlers.put(QueryGameAsk.class,         new QueryGameAskHandler(m_Context));
        /* Answer handler */
        m_Handlers.put(QueryGameAnswer.class,      new QueryGameAnswerHandler(m_Context));
        /* Voting handler */
        m_Handlers.put(QueryGameVote.class,        new QueryGameVoteHandler(m_Context));
        /* Users handler */
        m_Handlers.put(QueryUserHasInvites.class,  new QueryUserHasInvitesHandler(m_Context));
        m_Handlers.put(QueryUserAvailable.class,   new QueryUserAvailableHandler(m_Context));
        m_Handlers.put(QueryUserInvite.class,      new QueryUserInviteHandler(m_Context));
    }
    @SuppressWarnings("unchecked")
    private synchronized Serializable userQuery(final String clientID, final Object query) {
        final StickersQueryHandler handler = getHandler(query.getClass());
        if (handler == null) {
            LoggingUtility.error("Handler is not registered for " + query.getClass());
            return new ResponseCommonMessage("Internal server error: can't process: " + query.getClass());
        }
        return handler.processQuery(m_Context.getClientsManager().getClient(clientID), query);
    }
    private StickersQueryHandler getHandler(final Class c) {
        return m_Handlers.get(c);
    }
    private ServerResponse answer(final Serializable object) {
        return new ServerResponse(new Message(object));
    }
    @Override
    public boolean onConnect(final String clientID, final InetAddress clientAddress) {
        LoggingUtility.info("User <" + clientID + "> connected from " + clientAddress.getHostAddress());
        m_Context.getClientsManager().clientConnected(clientID);
        return true;
    }
    @Override
    public final ServerResponse onReceive(final String clientID, final Message message) {
        final Object object = message.getObject();
        if (object == null) {
            LoggingUtility.error("Unknown object accepted");
            return answer(new ResponseCommonMessage("Internal server error: empty object"));
        }
        return new ServerResponse(new Message(userQuery(clientID, object)));
    }
    @Override
    public void onDisconnect(final String clientID) {
        m_Context.getClientsManager().clientDisconnected(clientID);
        LoggingUtility.info("User <" + clientID + "> disconnected");
    }
    public void stop() {
        m_JobsManager.stop();
    }
}



This class contains the mapping of classes of request objects into corresponding handler objects. This approach (although it is not the fastest in execution time) allows, in my opinion, to organize the code well. Each handler solves only one specific task related to the request. For example, user registration.

User Registration Processor
package com.gesoftware.stickers.server.handlers.registration;
import com.gesoftware.stickers.model.enums.UserStatus;
import com.gesoftware.stickers.model.objects.User;
import com.gesoftware.stickers.model.queries.registration.QueryRegistration;
import com.gesoftware.stickers.model.responses.registration.ResponseRegistrationInvalidEMail;
import com.gesoftware.stickers.model.responses.registration.ResponseRegistrationFailed;
import com.gesoftware.stickers.model.responses.registration.ResponseRegistrationSuccessfully;
import com.gesoftware.stickers.model.responses.registration.ResponseUserAlreadyRegistered;
import com.gesoftware.stickers.server.handlers.StickersQueryHandler;
import com.gesoftware.stickers.server.managers.StickersManager;
import com.gesoftware.venta.logging.LoggingUtility;
import com.gesoftware.venta.utility.ValidationUtility;
import java.io.Serializable;
public final class QueryRegistrationHandler extends StickersQueryHandler {
    public QueryRegistrationHandler(final StickersManager context) {
        super(context);
    }
    @Override
    public final Serializable process(final User user, final QueryRegistration query) {
        if (!ValidationUtility.isEMailValid(query.m_EMail))
            return new ResponseRegistrationInvalidEMail();
        if (m_Context.getUsersManager().isUserRegistered(query.m_EMail))
            return new ResponseUserAlreadyRegistered();
        if (!m_Context.getUsersManager().registerUser(query.m_EMail, query.m_PasswordHash, query.m_Name))
            return new ResponseRegistrationFailed();
        LoggingUtility.info("User <" + user.m_ClientID + "> registered as " + query.m_EMail);
        return new ResponseRegistrationSuccessfully();
    }
    @Override
    public final UserStatus getStatus() {
        return UserStatus.NotLogged;
    }
}



The code is pretty easy to read, isn't it?

Client application

The client application implements exactly the same logic with handlers, but only server responses. It is implemented in a class inherited from the IClientHandler interface.

The number of different Activities is the same as the number of game states. The principle of interaction with the server is quite simple:
  • The user performs some action (for example, clicks the "Enter the game" button)
  • The client application displays a Progress dialog to the user.
  • Client application sends user credentials to server
  • The server processes the request and sends back a response
  • The handler corresponding to the answer hides the Progress dialog
  • The response is processed and the results are output to the client


Thus, the business logic on both the client and the server is divided into a large number of small structured classes.

Another thing I would like to talk about is in-app purchases. As has been seen in several articles here, in-app purchases are a pretty convenient solution for monetizing an application. I decided to take the advice and added advertising to the application and the ability to disable it for $ 1.

Когда я только начинал разбираться с биллингом, я убил огромное количество времени на осмысление принципа его работы в Google. Я достаточно долго времени пытался понять как же осуществить валидацию платежа на сервере, ведь логичным кажется после выдачи Google'ом некой информации о платеже (скажем, номер платежа), передать его на игровой сервер и уже с него, обратившись через API Google, проверить выполнен ли платёж. Как оказалось, такая схема работает только для подписок. Для обычных покупок все гораздо проще. При осуществлении покупки в приложении, Google возвращает JSON с информацией о покупке и её статусе (чек) и электронную подпись этого чека. Таким образом все упирается в вопрос «доверяете ли Вы компании Google?». :) Собственно, после получения такой пары, она пересылается на игровой сервер, которому только останется проверить две вещи:
  • Have you already sent such a request to the server (this is for operations uncontrolled by Google, say buying game currency)
  • Is the check correctly signed by electronic signature (after all, the common Google key is known to everyone, including the server)


On this note, I would like to finish my first and chaotic story. I read my article several times, I understand that this is not the ideal of a technical text, and perhaps it is difficult to understand, but in the future (if it does), I will try to correct the situation.

References



Third-party libraries



Conclusion

If someone had the patience to read through to the end, I express my gratitude, as I do not pretend to be a professional writer . I ask you to strongly criticize, as this is my first experience of publishing here. One of the reasons for the publication is the alleged “habraeffect”, which I need to conduct load testing of the server, as well as recruit a game audience, so I apologize for the selfish component of the publication’s purpose. I would be grateful for an indication of errors / inaccuracies. Thanks for attention!

In conclusion, a small survey (I can’t add it at the moment): is it worth publishing in the future? If so, on what topic would the publications be of interest:
  • Mathematics: Linear Algebra
  • Mathematics: Analysis
  • Mathematics: numerical and optimization methods
  • Mathematics: Discrete Mathematics and Algorithm Theory
  • Mathematics: Computational Geometry
  • Programming: the basics of computer graphics ( for example, this project )
  • Programming: Shader Programming
  • Programming: game development
  • Physics: Theory of Relativity
  • Physics: Astrophysics


What is where?

Also popular now: