About how we made the game for Google Play

learn how we made the game "stickers" for Google Play


Long ago I had the idea to share their knowledge with the community. At first I wanted to write something in astrophysics or General relativity, but decided that would be more correct to write about the subject area, which I do professionally. So, I will try to elaborate on the process of creating and the details of the implementation of the game application for Android (from design to publication and In App purchases).


Introduction


The programming I was doing with first class, graduated from the Applied mathematics St. Petersburg state Polytechnical University. Recently (a year ago) discovered the development of a mobile platform. I began to wonder what it is and what it eats. Currently developing several projects in a team of friends/colleagues, but I would like to write about my first experience. This experience was to write a game application — "stickers" (Who am I?).

What kind of stickers are these?
For those who do not know — I will explain. Stickers are such a drinking game in which each player on the forehead glued a piece of paper with some famous character (characters up with each other playing). The goal of each participant is to guess the hidden character to it.
The gameplay is an alternate assignment Yes/no questions and receive answers from other players.


The choice fell on "stickers" for several reasons.
First, we did not find analogues in the market (referring to the implementation of the rules described drinking games).
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 virtual.

the development Process


problem Statement

The task formed sufficiently unambiguous. You need to implement client-server application that allows its users to use the following features:
the
    the
  • to Create your own account
  • the
  • Authentication using your own account
  • the
  • View ranking
  • the
  • games room
  • the
  • Entrance to game room
  • the
  • Participation in the game process


Gameplay is a successive phase changes:
the
    the
  • Phase issues
  • the
  • Phase of voting


UI Design

Fortunately, my wife is a designer and I almost had to take part in the selection palette, layout and other design pieces. The results of the analysis of possibilities that the game should provide the player decided how much will be the game of the States (Activities) and what controls should be in each of them:
the
    the
  • Main menu
    the
      the
    • Authentication
    • the
    • Registration
    • the
    • Access to the player ranking

  • the
  • player rankings
    the
      the
    • Return to main menu

  • the
  • List of rooms
    the

      the entrance to the room the

    • Create your own room
    • the
    • main menu

  • the
  • Current room
    the
      the
    • exit the room

  • the
  • Game state # 1: start typing a question
    the
      the
    • enter the question
    • the
    • Go to the story questions
    • the
    • enter the answer
    • the
    • main menu

  • the
  • Game state # 2: viewing the history of questions
    the
      the
    • enter the question
    • the
    • enter the answer
    • the
    • main menu

  • the
  • Game state # 3: enter the answer
    the
      the
    • enter the answer
    • the
    • enter the question
    • the
    • Go to the story questions
    • the
    • main menu

  • the
  • Playing condition No. 4: the vote
    the
      the
    • Input votes
    • the
    • main menu

  • the
  • Victory
    the
      the
    • main menu

  • the
  • Defeat
    the
      the
    • Navigation in Wikipedia (on the character page)



Diagram of state transitions




DB Design

As soon as we became clear what the game States and objects exist in the game, we moved on to their formalization in terms of the database.

So, we will need the following tables:
the
    the
  • Users. A table that stores information about all users
  • the
  • Games. A table that stores information about all rooms
  • the
  • Characters. A table that stores information about all the characters
  • the
  • Questions. The table that stores users ' questions about them hidden characters
  • the
  • Answers. The table that stores the user's response


In the initial version of the game was only these tables, but the game has evolved and added a new one. I won't describe the rest of the table, otherwise the narrative is excessively delayed. The database schema shows all the tables, but their presence will not prevent future will tell.

database Schema



Relationships between tables has been changed several times (agile, so to speak), but in the end was left
the
    the
  • Each user can be assigned to one character
  • the
  • Each user can only be in one room
  • the
  • Each question can be specified by only one user
  • the
  • Each question can belong to only one character
  • the
  • Each question can be set only within the same game
  • the
  • Each answer can be given by only one user
  • the
  • Each answer can only be given one question

where is the data normalization?
communication is needed only to reduce load on the DBMS and they appeared far not the first version of the game. With the increase in the number of tables has increased the number of units to be produced for specific data samples.


application Level

Finally we got to program implementation. Let's start with the most common words. The entire project consists of 4 modules:
the
    the
  • Venta. Library, which contains useful utilities
  • the
  • Protocol. Library with a description of the interaction Protocol
  • the
  • Server. The app back-end
  • the
  • Client. The client part of the application


Schema design


Library Venta

Because I love to reinvent the wheel, and not like a hodgepodge of third-party libraries (Yes, the classic problem of many programmers-pedants), I decided to write some things myself. This library is written by me for a long time and it contains many useful utilities for me (working with database, client-server interaction, actors, math, encryption,...).
In this article I want to talk about the online portion of the library. The implementation of the interaction between the client and the server, I decided to do this by serializing/deserializing objects, among which are requests and responses. As the basic unit of transmitted information (at the library level, of course) is the object Message:

”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: Dmitry Eliseev
* */
public Message(final byte data[]) {
m_Timestamp = System.currentTimeMillis();
m_Data = data;
} /* End of 'Message::Message' method */

/* *

* PARAM: [IN] data - bytes array data
* AUTHOR: Dmitry Eliseev
* */
public Message(final String data) {
this(data.getBytes());
} /* End of 'Message::Message' method */

/* *
* METHOD: Message class constructor
* PARAM: [IN] object - some serializable object
* AUTHOR: Dmitry Eliseev
* */
public Message(final Object Object) {
this(CompressionUtility.compress(object));
} /* End of 'Message::Message' method */

/* *
* METHOD: data Bytes representation of the getter
* RETURN: Data bytes representation
* AUTHOR: Dmitry Eliseev
* */
public final byte[] getData() {
return m_Data;
} /* End of 'Message::getData' method */

/* *
* METHOD: Gets message size
* RETURN: Data size in bytes
* AUTHOR: Dmitry Eliseev
* */
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: Dmitry Eliseev
* */
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: Dmitry Eliseev
* */
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 the object, that may be packed in the message
* RETURN: the Restored object if success, null otherwise
* AUTHOR: Dmitry Eliseev
* */
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: Dmitry Eliseev
* */
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 the network occurs through the use of two classes:
the
    the
  • Server (server-side)
  • the
  • Connection (client)


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

”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 the connected client, false if reject
* PARAM: [IN] clientID - client identifier (store it somewhere)
* PARAM: [IN] clientAddress - connected client information
* AUTHOR: Dmitry Eliseev
* */
public abstract boolean onConnect(final String clientID, final InetAddress clientAddress);

/* *
* METHOD: Will be called right after server accept message from any connected client
* RETURN: the Response (see ServerResponse class), or null if you want to disconnect client
* PARAM: [IN] clientID - the sender identifier
* PARAM: [IN] message - received message
* AUTHOR: Dmitry Eliseev
* */
public abstract ServerResponse onReceive(final String clientID, final Message message);

/* *
* METHOD: Will be called right after any client is disconnected
* PARAM: [IN] clientID - the client identifier disconnected
* AUTHOR: Dmitry Eliseev
* */
public abstract void onDisconnect(final String clientID);
} /* End of 'IServerHandler' interface */



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

”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 the connected client, false if reject
* PARAM: [IN] clientID - client identifier (store it somewhere)
* PARAM: [IN] clientAddress - connected client information
* AUTHOR: Dmitry Eliseev
* */
public abstract boolean onConnect(final String clientID, final InetAddress clientAddress);

/* *
* METHOD: Will be called right after server accept message from any connected client
* RETURN: the Response (see ServerResponse class), or null if you want to disconnect client
* PARAM: [IN] clientID - the sender identifier
* PARAM: [IN] message - received message
* AUTHOR: Dmitry Eliseev
* */
public abstract ServerResponse onReceive(final String clientID, final Message message);

/* *
* METHOD: Will be called right after any client is disconnected

* AUTHOR: Dmitry Eliseev
* */
public abstract void onDisconnect(final String clientID);
} /* End of 'IServerHandler' interface */


Now a little about the inner workings of the server. Once the server joins another client, it calculates a unique hash and creates two flows: the flow of receiving and sending thread. The flow of the reception is blocked and waiting for a message from the client. As soon as the message from the client has been accepted, it is transmitted to a registered user of the library handler. As a result of processing can occur in one of five events:
the
    the
  • client Disconnection (for example, I received a request for shutdown)
  • the
  • customer reply
  • the
  • send the response to another client
  • the
  • send the response to all connected clients
  • the
  • sending a response to a group of customers


Now if you want to send the message to some of the connected clients, it is placed in the send queue of messages of the client, and the thread is responsible for sending notified that the queue has a new message.

Clearly, the data stream can be demonstrated by the diagram below.
data Flow in the network module library



Client X sends the request to the server (red arrow). This request is in accordance with client flow-receiver. He immediately calls the message handler (yellow arrow). As a result of processing, developing some response that is placed in the send queue of a client X (green arrow). A sending thread checks for messages in the send queue (black arrow) and sends the response to the client (blue arrow).

Example (multiuser 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();
}
}


Quite short, isn't it?

Game server

The architecture of the game server multilevel. Immediately give her a diagram and then a description.br>
diagram of the server architecture


So, to interact with the database connection pooling is used (I'm using the BoneCP library). To work with prepared queries (prepared statements), I wrapped the connection in your own class (library Venta).

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.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, connection created from
* 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 - the 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 - the 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: a String parameter setter
* RETURN: True if success, False otherwise
* PARAM: [IN] index - the 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 - the 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 - the 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 host Database service
* PARAM: [IN] port - Database service port
* PARAM: [IN] name - the 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 pool connections:" + 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 the statement using the current connection
* RETURN: Prepared statement
* PARAM: [IN] query SQL query
* AUTHOR: Dmitry Eliseev
* */
public final DBStatement preparestatement(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::preparestatement' method */

/* *
* METHOD: Closes the prepared statement
* PARAM: [IN] sql prepared statement
* AUTHOR: Dmitry Eliseev
* */
private void closeStatement(final query DBStatement) {
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 INSERT query like
* RETURN: the Inserted item identifier if success, 0 otherwise

* AUTHOR: Dmitry Eliseev
* */
public final long insert(final query DBStatement) {
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 UPDATE query like
* RETURN: True if success, False otherwise
* PARAM: [IN] sql prepared statement
* AUTHOR: Dmitry Eliseev
* */
public final boolean update(final query DBStatement) {
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 query DBStatement) {
final AbstractList<Map<String, Object>> 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<Map<String, Object>> select(final query DBStatement) {
try {
/* Container for result set */
final AbstractList<Map<String, Object>> results = new LinkedList<Map<String, Object>>();

/* Query execution */
query.m_Statement.execute();

/* Determine the 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<String, Object> row = new HashMap<String, Object>();

/* Copying data fetched */
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 */



More should pay attention to the class DBController.java:
DBController.java
package com.gesoftware.venta.db;

import com.gesoftware.venta.logging.LoggingUtility;

import java.util.*;

/**
* DB controller class definition
**/
public abstract class DBController<T> {
/* Real DB connection */
protected final DBConnection m_Connection;

/* *
* METHOD: Class constructor
* PARAM: [IN] connection - real DB connection
* AUTHOR: Dmitry Eliseev
* */
protected DBController(final connection DBConnection) {
m_Connection = connection;

LoggingUtility.core(getClass().getCanonicalName() + "controller initialized");
} /* End of 'DBController::DBController' method */

/* *
* METHOD: Requests a collection of T objects using select statement
* RETURN: Collection of objects if success, empty collection otherwise
* PARAM: [IN] a - a prepared select statement
* AUTHOR: Dmitry Eliseev
* */
protected final Collection<T> getCollection(final DBConnection.A DBStatement) {
if (a == null)
return new LinkedList<T>();

final AbstractList<Map<String, Object>> objectsCollection = m_Connection.select(a);
if ((objectsCollection == null)||(objectsCollection.size() == 0))
return new LinkedList<T>();

final Collection<T> parsedObjectsCollection = new ArrayList<T>(objectsCollection.size());
for (final Map<String, Object> object : objectsCollection)
parsedObjectsCollection.add(parse(object));

return parsedObjectsCollection;
} /* End of 'DBController::getCollection' method */

/* *
* METHOD: Requests one T object using the select statement
* RETURN: Object if success, null otherwise
* PARAM: [IN] a - a prepared select statement
* AUTHOR: Dmitry Eliseev
* */
protected final T getObject(final DBConnection.A DBStatement) {
if (a == null)
return null;

final AbstractList<Map<String, Object>> objectsCollection = m_Connection.select(a);
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 object T
* 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<String, Object> objectMap);
} /* End of 'DBController' class */



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

Some operations require changes to the data in several tables. This created a level managers. Every Manager has access to all controllers. At the level of managers implemented the operation of a higher level, for example, "Put user X in room A". In addition to the transition to a new abstraction level managers implement the caching mechanism of the data. For example, there is no need to go into the database whenever someone tries to get authenticated or wants to know your rating. Managers responsible for users or user rating this data is stored. Thus, the overall load on the database is reduced.
The next level of abstraction is handlers. As the implementation of the interface IserverHandler use the following class:
StickersHandler.java
package com.gesoftware.stickers.server.handlers;

import com.gesoftware.stickers.model.common.Definitions;

public final class StickersHandler implements IServerHandler {
private final Map<Class StickersQueryHandler> m_Handlers = new SynchronizedMap<a Class StickersQueryHandler>();
private final StickersManager m_Context;
private final JobsManager m_JobsManager;

public StickersHandler(final connection DBConnection) {
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. */
m_Handlers.put(QueryGameAsk.class new QueryGameAskHandler(m_Context));

/* Answer. */
m_Handlers.put(QueryGameAnswer.class new QueryGameAnswerHandler(m_Context));

/* Voting handler */
m_Handlers.put(QueryGameVote.class new QueryGameVoteHandler(m_Context));

/* Users. */
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 feature classes requests to the appropriate object handler. This approach (though he is not the fastest at run-time) allows, in my opinion, to organize the code. Each processor solves only one specific task associated with the request. For example, user registration.

Handler user registration
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<QueryRegistration> {
public QueryRegistrationHandler(final StickersManager context) {
super(context);
}

@Override
public final Serializable process(final User User, final query QueryRegistration) {
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 + "> is registered as " + query.m_EMail);
return new ResponseRegistrationSuccessfully();
}

@Override
public final UserStatus getStatus() {
return UserStatus.NotLogged;
}
}



Code reads quite easily, isn't it?

Client application

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

The number of different Activities coincides with the number of game States. The principle of interaction with the server is quite simple:
the
    the
  • a User performs some action (e.g., presses the button "Enter the game")
  • the
  • the Client application displays a Progress dialog to the user
  • the
  • the Client application sends to the server user credentials
  • the
  • the server processes the request and sends back a response
  • the
  • Corresponding to the response handler hides the Progress dialog
  • the
  • is processing the response and output the results to the client


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

Another thing I would like to tell you is in-app purchases. As noted in several articles here, quite a convenient solution for monetization of the app are in-app purchases. I decided to take the advice and added in-app advertising and the ability to disable it for$1.

When I started to deal with the billing, I killed a huge amount of time to reflect on the principle of its operation to Google. I long enough time trying to understand how to implement the validation of the payment on the server, because it seems logical after the issue Google some information about the payment (for example, the payment number), transfer it to the game server and already with him, asking through the Google API, to check whether the payment. As it turned out, this scheme works only for subscriptions. For ordinary purchases much easier. When implementing in-app purchase, Google returns a JSON with information about the purchase and its status (check) and electronic signature on this cheque. Thus everything rests on the question "do You trust Google?". :) Actually, after getting such a pair, it is forwarded to the game server, which only remain two things to check:
the
    the
  • Not sent if the server is already such a request (this is for nekontroliruem Google transactions, for example buying in-game currency)
  • the
  • Correctly check whether signed with an electronic signature (shared key because Google is known to all, including the server)


On that note, I would like to finish my first and muddled narrative. I read your article several times, I understand that this is not the ideal technical text, and perhaps it is quite difficult to understand, but in the future (if it comes), I'll try to fix the situation.

Links

the

third Party libraries

the

Conclusion

If someone had the patience to read to the end, I Express my gratitude, as it does not claim to be professional pisatelya. Please do criticize as this is my first experience publishing here. One of the reasons for publishing the alleged "gebrettert" I need to conduct load testing servers, and also set the gaming audience, so I apologize for the selfish component of the target publication. I would be grateful for pointing out errors/inaccuracies. Thank you for your attention!
In conclusion, a small poll (can't add at the moment): should in future be published? If Yes, on what topic would be more interesting publications:
the
    the
  • Mathematics: linear algebra
  • the
  • Mathematics: analysis
  • the
  • Mathematics: numerical methods and optimization techniques
  • the
  • Math: discrete mathematics and theory of algorithms
  • the
  • Math: computational geometry
  • the
  • Programming: fundamentals of computer graphics (example project)
  • the
  • Programming: programming Shader
  • the
  • Programming: game development
  • the
  • Physics: theory of relativity
  • the
  • Physics: astrophysics


What where?
the
Article based on information from habrahabr.ru

Популярные сообщения из этого блога

Approval of WSUS updates: import, export, copy

The Hilbert curve vs. Z-order

Kaspersky Security Center — the fight for automation