Google Play甚のゲヌムの䜜成方法に぀いお

Google Playのステッカヌの䜜成方法に぀いお





長い間、私は自分の知識をコミュニティず共有するこずを考えおいたした。 最初は、倩䜓物理孊やGTRに぀いお䜕かを曞きたかったのですが、それでも私は専門的に扱っおいるその䞻題分野に぀いお曞く方が正しいず刀断したした。 そのため、Android向けゲヌムアプリケヌションの䜜成プロセスず実装の埮劙な点蚭蚈から公開、アプリ内賌入たでを詳现に説明しようずしたす。





はじめに



私は最初のクラスからプログラミングに埓事し、サンクトペテルブルク州立工科倧孊の応甚数孊を卒業したした。 最近玄1幎前、モバむルプラットフォヌムの開発を発芋したした。 それが䜕で、䜕で食べられるのかが面癜くなっおきたした。 珟圚、友人/同僚のチヌムでいく぀かのプロゞェクトを開発しおいたすが、最初の経隓に぀いお曞きたいず思いたす。 そのような経隓は、ゲヌムアプリケヌション-「 ステッカヌ 」私は誰ですかを䜜成するこずでした。



これらはどのようなステッカヌですか
知らない人のために-私は説明したす。 ステッカヌ-これはこのようなテヌブルゲヌムで、各プレむダヌは額に有名なキャラクタヌがいる玙を1枚ず぀受け取りたすプレむするキャラクタヌは互いに登堎したす。 各参加者の目暙は、自分が掚枬したキャラクタヌを掚枬するこずです。

ゲヌムプレむは、質問の連続的なyes / noタスクであり、他のプレむダヌからそれらぞの回答を受け取りたす。





いく぀かの理由により、ステッカヌが遞ばれたした。

たず、垂堎に類䌌物が芋぀かりたせんでしたこれは、説明したテヌブルゲヌムのルヌルの実装を指したす。

第二に、私は非垞に時間がかからないものを曞きたかった。

第䞉に、このゲヌムは私たちのサヌクルで非垞に人気があり、おそらく誰かがそれを実際にプレむしたいず思うず思いたした。



開発プロセス



問題の声明


タスクは非垞に明確に圢成されたした。 ナヌザヌが次の機胜を䜿甚できるようにするクラむアント/サヌバヌアプリケヌションを実装する必芁がありたす。





ゲヌムプレむは盞倉化です





UIデザむン


幞いなこずに、私の劻はデザむナヌであり、パレット、芁玠の配眮、その他のデザむナヌのものを遞択するこずに実際に参加する必芁はありたせんでした。 ゲヌムがプレヌダヌに提䟛する必芁がある機胜の分析に基づいお、ゲヌムの状態アクティビティの数ず各ゲヌムの状態に必芁なコントロヌルを決定したした。





状態遷移図








DBデザむン


ゲヌム内にどのゲヌム状態ずオブゞェクトが存圚するかが明らかになったらすぐに、デヌタベヌスの芳点からそれらを圢匏化するこずに進みたした。



そのため、次の衚が必芁です。





ゲヌムの初期バヌゞョンにはこれらのテヌブルしかありたせんでしたが、ゲヌムが開発され、新しいテヌブルが远加されたした。 残りのテヌブルに぀いおは説明したせん。そうしないず、ナレヌションが非垞に長くなりたす。 すべおのテヌブルはデヌタベヌスダむアグラムに衚瀺されたすが、テヌブルが存圚しおもこれ以䞊の議論を劚げるこずはありたせん。



デヌタベヌススキヌマ






テヌブル間の関係は数回いわばアゞャむル倉曎されたしたが、最終的には次のようになりたした。



そしお、デヌタの正芏化はどこにありたすか
重耇した通信は、DBMSの負荷を軜枛するためにのみ必芁であり、ゲヌムの最初のバヌゞョンからはほど遠いように芋えたした。 テヌブルの数の増加に䌎い、特定のデヌタサンプルに察しお実行する必芁のある集蚈の数が増加したした。





アプリケヌションレベル


最埌に、゜フトりェアの実装に到達したした。 だから、私は最も䞀般的な蚀葉から始めたす。 プロゞェクト党䜓は4぀のモゞュヌルで構成されおいたす。





プロゞェクト抂芁








ベンタ図曞通


私は車茪を再発明するのが奜きで、サヌドパヌティのラむブラリのごたかしが嫌いなのでそう、倚くの退屈なプログラマヌの叀兞的な問題、私は自分で䜕かを曞くこずにしたした。 私はこのラむブラリを長い間曞いおおり、私にずっお倚くの有甚なナヌティリティが含たれおいたすデヌタベヌス、クラむアント/サヌバヌ盞互䜜甚、アクタヌ、数孊、暗号化など。

この蚘事では、このラむブラリのネットワヌク郚分に぀いおお話したいず思いたす。 私は、リク゚ストず回答があるオブゞェクトをシリアラむズ/デシリアラむズするこずにより、クラむアントずサヌバヌ間の盞互䜜甚を実装するこずにしたした。 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: 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 */
      
      









このオブゞェクトの説明に぀いおは詳しく説明したせんが、コヌドは非垞にコメントされおいたす。



ネットワヌクでの䜜業の簡玠化は、次の2぀のクラスの䜿甚によるものです。





タむプServerのオブゞェクトを䜜成する堎合、着信接続を埅機するポヌトず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 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 */
      
      









クラむアントは、 Connectionタむプのオブゞェクトを䜜成するずきに、 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 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 */
      
      







サヌバヌの内郚構造に぀いお少し説明したす。 次のクラむアントがサヌバヌに参加するずすぐに、䞀意のハッシュが蚈算され、受信ストリヌムず送信ストリヌムの2぀のストリヌムが䜜成されたす。 受信ストリヌムはブロックされ、クラむアントからのメッセヌゞを埅ちたす。 クラむアントからのメッセヌゞが受信されるずすぐに、ラむブラリナヌザヌが登録したハンドラヌに送信されたす。 凊理の結果、次の5぀のむベントのいずれかが発生する堎合がありたす。





接続されたクラむアントの1぀にメッセヌゞを送信する必芁がある堎合、このクラむアントのメッセヌゞ送信キュヌに配眮され、送信を担圓するスレッドに新しいメッセヌゞがキュヌに衚瀺されたこずが通知されたす。



明らかに、デヌタフロヌは次の図で瀺すこずができたす。

ラむブラリのネットワヌクモゞュヌルでのデヌタフロヌ






クラむアントXはサヌバヌにリク゚ストを送信したす赀い矢印。 芁求は、クラむアントに察応するレシヌバヌストリヌムで受信されたす。 すぐにメッセヌゞハンドラを呌び出したす黄色の矢印。 凊理の結果、特定の応答が圢成され、クラむアントXの送信キュヌに配眮されたす緑色の矢印。 送信ストリヌムは、送信キュヌ内のメッセヌゞを確認し黒い矢印、クラむアントに応答を送信したす青い矢印。



䟋マルチナヌザヌ゚コヌサヌバヌ
 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(); } }
      
      







かなり短いですね。



ゲヌムサヌバヌ


ゲヌムサヌバヌのアヌキテクチャはマルチレベルです。 すぐに私は圌女のスキヌムず、その埌の説明をしたす。

サヌバヌアヌキテクチャ図








そのため、デヌタベヌスずの察話には接続プヌルが䜿甚されたす私はBoneCPラむブラリを䜿甚しおいたす。 プリペアドステヌトメントを䜿甚するために、接続を自分のクラス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.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 */
      
      









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 */
      
      









DBControllerクラスは、特定のテヌブルのオブゞェクトを操䜜するように蚭蚈されおいたす。 サヌバヌアプリケヌションでは、デヌタベヌステヌブルごずにコントロヌラヌが䜜成されたす。 コントロヌラレベルでは、デヌタベヌスのデヌタを挿入、抜出、曎新するためのメ゜ッドが実装されたす。



䞀郚の操䜜では、耇数のテヌブルのデヌタを䞀床に倉曎する必芁がありたす。 このために、マネヌゞャヌレベルが䜜成されたした。 各マネヌゞャヌは、すべおのコントロヌラヌにアクセスできたす。 マネヌゞャヌレベルでは、たずえば「ナヌザヌXを郚屋Aに配眮する」などの高レベルの操䜜が実装されたす。 新しいレベルの抜象化ぞの移行に加えお、マネヌゞャヌはデヌタキャッシングメカニズムを実装したす。 たずえば、誰かが認蚌を詊みたり、評䟡を知りたい堎合は、デヌタベヌスにアクセスする必芁はありたせん。 ナヌザヌたたはナヌザヌ評䟡を担圓するマネヌゞャヌがこのデヌタを保存したす。 したがっお、デヌタベヌスの党䜓的な負荷が軜枛されたす。



抜象化の次のレベルはハンドラヌです。 次のクラスは、IserverHandlerむンタヌフェむスの実装ずしお䜿甚されたす。

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









このクラスには、芁求オブゞェクトのクラスの察応するハンドラヌオブゞェクトぞのマッピングが含たれたす。 私の意芋では、このアプロヌチにより実行時間は最速ではありたせんがコヌドを適切に線成できたす。 各ハンドラヌは、リク゚ストに関連する特定のタスクを1぀だけ解決したす。 たずえば、ナヌザヌ登録。



ナヌザヌ登録プロセッサヌ
 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; } }
      
      









コヌドは非垞に読みやすいですよね



クラむアントアプリケヌション


クラむアントアプリケヌションは、ハンドラヌずたったく同じロゞックを実装したすが、サヌバヌの応答のみを実装したす。 IClientHandlerむンタヌフェむスから継承されたクラスに実装されたす。



異なるアクティビティの数は、ゲヌムの状態の数ず同じです。 サヌバヌずの察話の原理は非垞に簡単です。





したがっお、クラむアントずサヌバヌの䞡方のビゞネスロゞックは、倚数の小さな構造化クラスに分割されたす。



もう1぀お話ししたいのは、アプリ内賌入です。 ここでいく぀かの蚘事で芋たように、アプリ内賌入はアプリケヌションを収益化するための非垞に䟿利な゜リュヌションです。 私はアドバむスをするこずにし、アプリケヌションに広告を远加し、$ 1で無効にする機胜を远加したした。



課金の凊理を始めたばかりの頃、私はそれがGoogleでどのように機胜するかを考えるのに膚倧な時間を費やしたした。 Googleが支払いに関する情報支払い番号などを発行し、ゲヌムサヌバヌに転送し、それから既にGoogle APIに連絡しお確認を完了した埌、論理的に思えるので、長い間サヌバヌで支払いを怜蚌する方法を理解しようずしたした支払いかどうか。 結局のずころ、そのようなスキヌムはサブスクリプションに察しおのみ機胜したす。 通垞の賌入では、すべおがはるかに簡単です。 アプリケヌションで賌入を行うず、Googleは賌入ずそのステヌタスチェックおよびこのチェックの電子眲名に関する情報を含むJSONを返したす。 したがっお、すべおは「Googleを信頌しおいたすか」ずいう質問にかかっおいたす。 :)実際、そのようなペアを受信した埌、ゲヌムサヌバヌに送信されたす。ゲヌムサヌバヌは、次の2぀のこずを確認するだけで枈みたす。





このメモでは、最初の混firstずした話を終わらせたいず思いたす。 私は自分の蚘事を䜕床か読みたしたが、これは技術的なテキストの理想ではなく、おそらく理解するのは難しいかもしれたせんが、将来もしそうなら、状況を修正しようずしたす。



参照資料






サヌドパヌティのラむブラリ






おわりに


誰かが最埌たで読む忍耐を持っおいた堎合、私はプロの䜜家のふりをしおいないので、感謝を衚したす。ここで公開するのはこれが初めおなので、匷く批刀しおください。この出版の理由の1぀は、サヌバヌの負荷テストを行う必芁があるだけでなく、ゲヌムの芖聎者を募集する必芁があるず蚀われる「habraeffect」です。゚ラヌ/䞍正確さの兆候に感謝したす。ご枅聎ありがずうございたした



結論ずしお、小さなアンケヌト珟時点では远加できたせん将来公開する䟡倀はありたすかもしそうなら、どのトピックで出版物が興味を持ちたすか





どこ




All Articles