📜 ⬆️ ⬇️

About how we made the game for google play

About how we made the game "Stickers" for Google Play


Long ago, I had the idea to share my knowledge with the community. At first I wanted to write something on astrophysics or GR, but I decided nevertheless that it would be more correct to write about the subject area 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 (starting from design, ending with publication and In App purchases).


Introduction


I am engaged in programming from the first class, I graduated from Applied Mathematics of St.Petersburg State Pedagogical University Recently (about a year ago) I discovered development for mobile platforms. It became interesting what it is and what it is eaten with. Currently I am 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 gaming application - “ Stickers ” (Who am I?).
')
What kind of stickers are these?
For those who do not know - I will explain. Stickers are such a table game, in which each player has a piece of paper glued to his forehead with some famous character (the characters come up with each other playing). The goal of each participant is to guess the character he has guessed.
Gameplay is an alternate task of "yes / no" questions and receiving answers to them from other players.


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

Development process


Formulation of the problem

The task was formed quite unambiguous. You need to implement a client-server application that allows its users to use the following features:


Gameplay is an alternate phase change:


UI Design

Fortunately, my wife is a designer and I practically did not have to take part in the selection of the palette, the arrangement of elements and other design pieces. Based on the analysis of the possibilities that the game should provide the player, it was decided how many game states (Activities) there would be and what controls should be in each of them:


State transition pattern




DB Design

As soon as it became clear to us which game states and objects exist in the game, we proceeded to formalize them in terms of the database.

So, we need the following tables:


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 overly long. The database diagram shows all the tables, but their presence will not prevent further tell.

Database schema



The relationships between the tables changed several times (agile, so to speak), but eventually the following remained:

And where is the data normalization?
Duplicate links are needed only to reduce the load on the database and they appeared far from the first version of the game. With the increase in the number of tables, the number of aggregations has increased, which needs to be made for certain data samples.


Application level

Finally we got to the software implementation. So, I'll start with the most common words. The whole project consists of 4 modules:


Project outline


Venta Library

Since I like to reinvent the wheel and don’t like the jumble of third-party libraries (yes, yes, the classic problem of many pedant programmers), I decided to write some things myself. This library is written by me for a long time and it contains many utilities useful for me (work 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 the server by serializing / deserializing the objects, among which are requests and responses. The elementary information item being sent (at the library level, of course) is the Message object:

"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 elaborate on the description of this object, the code is quite commented.

Simplification of work with the network occurs through the use of two classes:


When creating an object of type Server , you need to specify the port on which it will expect 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 device 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. Reception flow is blocked and waiting for a message from the client. As soon as the message from the client has been accepted, it is transferred to the handler registered by the library user. As a result of processing, one of five events can occur:


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

Clearly, the data stream can be shown below.
The data flow in the network module of the library



Client X sends a request to the server (red arrow). The request is received at the corresponding receiver stream. It immediately calls the message handler (yellow arrow). As a result of processing, some response is generated, which is placed in the client's send queue 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 game server architecture is multi-layered. Immediately give her scheme, and then the description.
Server architecture diagram


So, the connection pool is used to interact with the database (I use the BoneCP library). To work with prepared requests (prepared statements), I wrapped the connection in my own class (the 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<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 DBStatement query) { try { /* Container for result set */ final AbstractList<Map<String, Object>> results = new LinkedList<Map<String, Object>>(); /* 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<String, Object> row = new HashMap<String, Object>(); /* 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 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 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<T> getCollection(final DBConnection.DBStatement selectStatement) { if (selectStatement == null) return new LinkedList<T>(); final AbstractList<Map<String, Object>> objectsCollection = m_Connection.select(selectStatement); 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 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<Map<String, Object>> 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<String, Object> objectMap); } /* End of 'DBController' class */ 



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

Some operations require changing data in several tables at once. To this end, the level of managers created. Each manager has access to all controllers. At the managerial level, higher level operations are implemented, for example, “Put user X into 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 climb into the database whenever someone tries to authenticate or wants to know their rating. The managers responsible for the users or the rating of the users keep these data. Thus, the total load on the database is reduced.

The next level of abstraction is handlers. The following class is used as the 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<Class, StickersQueryHandler> m_Handlers = new SynchronizedMap<Class, StickersQueryHandler>(); 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 to corresponding handler objects. This approach (although it is not the fastest in terms of execution time) allows, in my opinion, to organize code well. Each handler solves only one specific task associated with the request. For example, user registration.

User Registration Handler
 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 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 quite 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 coincides with the number of game states. The principle of interaction with the server is quite simple:


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

One more thing I would like to tell about is the in-app purchases. As noted in several articles here, a fairly convenient solution for monetizing an app is in-app purchases. I decided to use the advice and added advertising to the application and the ability to disable it for $ 1.

When I first began to deal with billing, I killed a huge amount of time to understand the principle of its work in Google. For a long time I tried to understand how to validate a payment on the server, after all, it seems logical after Google issues some payment information (say, payment number) to transfer it to the game server and check it using the Google API. whether payment. As it turned out, this scheme works only for subscriptions. For ordinary purchases, everything is much simpler. When making purchases in the application, Google returns JSON with information about the purchase and its status (check) and the electronic signature of this check. So it all comes down to the question "Do you trust Google?". :) Actually, after receiving such a pair, it is sent to the game server, which will only have to check two things:


On this note, I would like to finish my first and confused story. I have read my article several times, I understand that this is not an ideal technical text, and perhaps it is difficult enough for perception, but in the future (if it comes), I will try to correct the situation.

Links



Third Party Libraries



Conclusion

If someone had the patience to finish reading to the end, I express my gratitude, since I do not pretend to be a professional writer . Please scold me a lot, as this is my first publishing experience here. One of the reasons for publishing is the alleged “habraeffect”, which I need to conduct load testing of the server, as well as a set of game audience, so I apologize for the mercenary component of the purpose of the publication. I would appreciate an indication of errors / inaccuracies. Thanks for attention!

In conclusion, a small survey (I can’t add it at the moment): should it be published later? If so, what topic would be interesting for publications:


What do you mean where?

Source: https://habr.com/ru/post/198054/


All Articles