Google Play के लिए हमने कैसे गेम बनाया है

हमने Google Play के लिए स्टिकर कैसे बनाए हैं


लंबे समय से मुझे अपने ज्ञान को समुदाय के साथ साझा करने का विचार था। पहले मैं एस्ट्रोफिजिक्स या जीटीआर पर कुछ लिखना चाहता था, लेकिन मैंने फिर भी तय किया कि उस विषय क्षेत्र के बारे में लिखना अधिक सही होगा, जिसे मैं पेशेवर रूप से देखता हूं। इसलिए, मैं बनाने की प्रक्रिया और एंड्रॉइड के लिए गेम एप्लिकेशन के कार्यान्वयन की सूक्ष्मताओं (डिजाइन से प्रकाशन तक और ऐप खरीदारी में) की विस्तार से व्याख्या करने की कोशिश करूंगा।


परिचय


मैं पहली कक्षा के बाद से प्रोग्रामिंग में लगा हुआ हूं, मैंने सेंट पीटर्सबर्ग स्टेट पॉलिटेक्निक यूनिवर्सिटी के एप्लाइड गणित से स्नातक किया है। हाल ही में (लगभग एक साल पहले) मैंने मोबाइल प्लेटफॉर्म के लिए विकास की खोज की। यह दिलचस्प हो गया कि यह क्या है और इसके साथ क्या खाया जाता है। मैं वर्तमान में दोस्तों / सहयोगियों की एक टीम में कई परियोजनाओं का विकास कर रहा हूं, लेकिन मैं अपने पहले अनुभव के बारे में लिखना चाहूंगा। ऐसा अनुभव एक गेम एप्लिकेशन लिख रहा था - " स्टिकर " (हू एम आई?)।

ये किस प्रकार के स्टिकर हैं?
जो लोग नहीं जानते हैं उनके लिए - मैं समझाता हूँ। स्टिकर - यह एक ऐसा टेबल गेम है जिसमें प्रत्येक खिलाड़ी को अपने माथे पर कुछ प्रसिद्ध चरित्र के साथ कागज का एक टुकड़ा मिलता है (पात्रों को एक दूसरे को खेलने से आविष्कार किया जाता है)। प्रत्येक प्रतिभागी का लक्ष्य उस चरित्र का अनुमान लगाना है जो उसने अनुमान लगाया है।
गेमप्ले एक अनुक्रमिक हां / कोई सवाल का काम नहीं है और अन्य खिलाड़ियों से उनके उत्तर प्राप्त कर रहा है।


विकल्प कई कारणों से स्टिकर पर गिर गया।
सबसे पहले, हमें बाजार में कोई एनालॉग नहीं मिला (यह वर्णित टेबल गेम के नियमों के कार्यान्वयन को संदर्भित करता है)।
दूसरे, मैं बहुत समय लेने वाली नहीं कुछ लिखना चाहती थी।
तीसरा, खेल हमारे हलकों में काफी लोकप्रिय है और हमने सोचा कि शायद किसी को इसे खेलने के लिए और वस्तुतः दिलचस्पी होगी।

विकास की प्रक्रिया


समस्या का बयान

यह कार्य काफी असमान था। क्लाइंट-सर्वर एप्लिकेशन को लागू करना आवश्यक है जो अपने उपयोगकर्ताओं को निम्नलिखित सुविधाओं का उपयोग करने की अनुमति देता है:


गेमप्ले एक चरण परिवर्तन है:


यूआई डिजाइन

सौभाग्य से, मेरी पत्नी एक डिजाइनर है और मुझे व्यावहारिक रूप से पैलेट, तत्वों की व्यवस्था और अन्य डिजाइनर चीजों को चुनने में भाग नहीं लेना है। खेल को खिलाड़ी को प्रदान करने वाली क्षमताओं के विश्लेषण के आधार पर, यह निर्णय लिया गया कि कितने खेल राज्य (गतिविधियाँ) होंगे और उनमें से प्रत्येक में क्या नियंत्रण होना चाहिए:


राज्य संक्रमण आरेख




DB डिजाइन

जैसे ही यह हमारे लिए स्पष्ट हो गया कि खेल राज्य और वस्तुएं किस खेल में मौजूद हैं, हम डेटाबेस के संदर्भ में उन्हें औपचारिक रूप देने के लिए आगे बढ़े।

तो, हमें निम्न तालिकाओं की आवश्यकता है:


खेल के शुरुआती संस्करण में केवल ये टेबल थे, लेकिन खेल विकसित हुआ और नए जोड़े गए। मैं बाकी तालिकाओं का वर्णन नहीं करूंगा, अन्यथा कथन अत्यधिक लंबा होगा। डेटाबेस आरेख में सभी तालिकाओं को दिखाया गया है, लेकिन उनकी उपस्थिति आगे की चर्चा में बाधा नहीं बनेगी।

डेटाबेस स्कीमा



तालिकाओं के बीच संबंध कई बार बदल गए (फुर्तीले, बोलने के लिए), लेकिन अंत में निम्नलिखित बने रहे:

और डेटा का सामान्यीकरण कहां है?
डीबीएमएस पर लोड को कम करने के लिए केवल डुप्लिकेट संचार की आवश्यकता होती है और वे खेल के पहले संस्करण से दूर दिखाई देते हैं। तालिकाओं की संख्या में वृद्धि के साथ, कुछ डेटा नमूनों के लिए एकत्रीकरण की संख्या में वृद्धि हुई है।


आवेदन स्तर

अंत में, हम सॉफ्टवेयर कार्यान्वयन के लिए तैयार हो गए। तो, मैं सबसे सामान्य शब्दों के साथ शुरू करूंगा। पूरी परियोजना में 4 मॉड्यूल शामिल हैं:


परियोजना की रूपरेखा


वेंटा लाइब्रेरी

चूंकि मैं पहिया को फिर से मजबूत करना चाहता हूं और तीसरे पक्ष के पुस्तकालयों (हां, हां, कई पैदल प्रोग्रामरों की एक क्लासिक समस्या) की परेशानियों की तरह नहीं है, मैंने खुद कुछ चीजें लिखने का फैसला किया। मैं इस पुस्तकालय को लंबे समय से लिख रहा हूं और इसमें मेरे लिए (डेटाबेस, क्लाइंट-सर्वर इंटरैक्शन, एक्टर्स, गणित, एन्क्रिप्शन ...) के साथ कई उपयोगी उपयोगिताओं का समावेश है।
इस लेख में, मैं इस पुस्तकालय के नेटवर्क भाग के बारे में बात करना चाहता हूं। मैंने क्लाइंट और सर्वर के बीच वस्तुओं को क्रमबद्ध / deserializing द्वारा बातचीत को लागू करने का निर्णय लिया, जिसके बीच अनुरोध और उत्तर हैं। संदेश वस्तु प्राथमिक सूचना इकाई (स्थान के स्तर पर, निश्चित रूप से) हस्तांतरित की जा रही है:

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



मैं इस ऑब्जेक्ट के विवरण पर ध्यान नहीं दूंगा, कोड काफी टिप्पणी की गई है।

नेटवर्क के साथ काम का सरलीकरण दो वर्गों के उपयोग के कारण होता है:


प्रकार सर्वर का एक ऑब्जेक्ट बनाते समय, आपको उस पोर्ट को निर्दिष्ट करना होगा जिस पर वह आने वाले कनेक्शन की प्रतीक्षा करेगा और 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 */ 



क्लाइंट, बदले में, प्रकार कनेक्शन का ऑब्जेक्ट बनाते समय, 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 */ 


अब सर्वर की आंतरिक संरचना के बारे में थोड़ा। जैसे ही अगला क्लाइंट सर्वर से जुड़ता है, उसके लिए एक अद्वितीय हैश की गणना की जाती है और दो स्ट्रीम बनाई जाती हैं: प्राप्त स्ट्रीम और सेंड स्ट्रीम। प्राप्त स्ट्रीम अवरुद्ध है और क्लाइंट से एक संदेश का इंतजार कर रहा है। जैसे ही क्लाइंट से संदेश प्राप्त हुआ है, यह पुस्तकालय उपयोगकर्ता द्वारा पंजीकृत हैंडलर को प्रेषित किया जाता है। प्रसंस्करण के परिणामस्वरूप, पाँच में से एक घटना हो सकती है:


यदि अब कनेक्टेड क्लाइंट में से किसी एक को संदेश भेजना आवश्यक है, तो उसे इस ग्राहक की कतार भेजने वाले संदेश में रखा जाता है, और भेजने के लिए जिम्मेदार सूत्र को सूचित किया जाता है कि नए संदेश कतार में दिखाई दिए हैं।

स्पष्ट रूप से, डेटा प्रवाह नीचे चित्र द्वारा प्रदर्शित किया जा सकता है।
पुस्तकालय के नेटवर्क मॉड्यूल में डेटा प्रवाह



क्लाइंट 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 लाइब्रेरी का उपयोग करता हूं)। तैयार बयानों के साथ काम करने के लिए, मैंने कनेक्शन को अपनी कक्षा (वेंटा लाइब्रेरी) में लपेटा।

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 क्लास को किसी विशेष टेबल की वस्तुओं के साथ काम करने के लिए डिज़ाइन किया गया है। सर्वर अनुप्रयोग में, डेटाबेस तालिकाओं में से प्रत्येक के लिए नियंत्रक बनाए जाते हैं। नियंत्रक स्तर पर, डेटाबेस में डेटा डालने, पुनर्प्राप्त करने, अद्यतन करने के तरीके लागू किए जाते हैं।

कुछ ऑपरेशनों में एक साथ कई तालिकाओं में डेटा बदलने की आवश्यकता होती है। इसके लिए, एक प्रबंधक स्तर बनाया गया है। प्रत्येक प्रबंधक की पहुंच सभी नियंत्रकों तक होती है। प्रबंधक स्तर पर, उच्च-स्तर के संचालन को लागू किया जाता है, उदाहरण के लिए, "कमरा ए में उपयोगकर्ता एक्स रखें"। अमूर्तता के एक नए स्तर पर जाने के अलावा, प्रबंधक एक डेटा कैशिंग तंत्र को लागू करते हैं। उदाहरण के लिए, डेटाबेस में जाने की आवश्यकता नहीं है जब भी कोई प्रमाणित करने की कोशिश करता है या अपनी रेटिंग जानना चाहता है। उपयोगकर्ता या उपयोगकर्ता रेटिंग के लिए जिम्मेदार प्रबंधक इस डेटा को संग्रहीत करते हैं। इस प्रकार, डेटाबेस पर समग्र भार कम हो जाता है।

अमूर्तता का अगला स्तर हैंडलर है। निम्न वर्ग 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(); } } 



इस वर्ग में संबंधित हैंडलर ऑब्जेक्ट्स में अनुरोध ऑब्जेक्ट्स की मैपिंग होती है। यह दृष्टिकोण (हालांकि यह निष्पादन के समय में सबसे तेज़ नहीं है), मेरी राय में, कोड को अच्छी तरह से व्यवस्थित करने की अनुमति देता है। प्रत्येक हैंडलर अनुरोध से संबंधित केवल एक विशिष्ट कार्य को हल करता है। उदाहरण के लिए, उपयोगकर्ता पंजीकरण।

उपयोगकर्ता पंजीकरण प्रोसेसर
 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 के लिए इसे निष्क्रिय करने की क्षमता का फैसला किया।

जब मैं बिलिंग से निपटना शुरू कर रहा था, तो मैंने यह सोचने में बहुत समय लगा दिया कि यह Google पर कैसे काम करता है। लंबे समय से मैं यह समझने की कोशिश कर रहा था कि किसी सर्वर पर भुगतान कैसे मान्य किया जाए, क्योंकि ऐसा लगता है कि Google द्वारा भुगतान (कहना, भुगतान नंबर) के बारे में कुछ जानकारी जारी करने के बाद, इसे गेम सर्वर पर स्थानांतरित कर दिया जाए, और इससे Google API से संपर्क करके, जाँच करें चाहे भुगतान हो। जैसा कि यह निकला, ऐसी योजना केवल सदस्यता के लिए काम करती है। साधारण खरीद के लिए, सब कुछ बहुत सरल है। एप्लिकेशन में खरीदारी करते समय, Google JSON को खरीद और उसकी स्थिति (चेक) और इस चेक के इलेक्ट्रॉनिक हस्ताक्षर के बारे में जानकारी देता है। इस प्रकार, सब कुछ प्रश्न पर निर्भर करता है "क्या आप Google पर भरोसा करते हैं?"। :) वास्तव में, इस तरह की जोड़ी प्राप्त करने के बाद, इसे गेम सर्वर पर भेजा जाता है, जिसमें केवल दो चीजों की जांच करनी होगी:


इस नोट पर, मैं अपनी पहली और अराजक कहानी समाप्त करना चाहूंगा। मैंने अपने लेख को कई बार पढ़ा, मैं समझता हूं कि यह एक तकनीकी पाठ का आदर्श नहीं है, और शायद यह समझना मुश्किल है, लेकिन भविष्य में (यदि ऐसा होता है), मैं स्थिति को ठीक करने का प्रयास करूंगा।

संदर्भ



तृतीय-पक्ष पुस्तकालय



निष्कर्ष

अगर किसी को अंत तक पढ़ने का धैर्य था, तो मैं आभार व्यक्त करता हूं, क्योंकि मैं एक पेशेवर लेखक होने का दिखावा नहीं करता मैं आपको बहुत डांटने के लिए कहता हूं, क्योंकि यहां प्रकाशित करने का यह मेरा पहला अनुभव है। प्रकाशन के कारणों में से एक "हेब्राफ़ेक्ट" है जिसे मुझे सर्वर के लोड परीक्षण का संचालन करने की आवश्यकता है, साथ ही एक खेल दर्शकों की भर्ती करने की भी आवश्यकता है, इसलिए मैं प्रकाशन के उद्देश्य के स्वार्थी घटक के लिए माफी माँगता हूँ। मैं त्रुटियों / अशुद्धियों के संकेत के लिए आभारी रहूंगा। आपका ध्यान के लिए धन्यवाद!

अंत में, एक छोटा सा सर्वेक्षण (फिलहाल मैं इसे जोड़ नहीं सकता): क्या यह भविष्य में प्रकाशित होने लायक है? यदि ऐसा है, तो प्रकाशन किस विषय पर होगा:


कहाँ क्या?

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


All Articles