Tales Of Fortune is a multiplayer online naval combat turn based game. The gameplay is to competitively and strategically plan out moves to beat your online enemies in a 20-30 minute long session game. The game will mainly be oriented around PvP but will also feature PvE in the form of AI enemies and bosses.
This entire game is a two man project which my partner (Link to partner) and I have pair programmed and combined our efforts instead of working on seperate elements of the game. This is mainly due to the fact that we are still coding the main framework for the game and we have considered splitting the work up for efficiency in the future. Since this is a stand-alone two man project we both function as jack of all trades working on all different aspects of the game such as graphics, shaders, post processing and lighting, camera, audio etc.
The main gameplay loop is every player (ship) gets to cash in 4 moves every round, moves include cannon shots. So prediction is the main strategic element in the game. We aim to keep the gameplay fluent and give room for the players to use their creativity to edge out the other players.
Big parts of the project were designed in a data oriented way because handling data packets this way was very optimal for us and simplified our code and communication between server and client immensely. We extracted most data out and used player IDs to identify which data belonged to what player, all data were modified using these indexes. This firstly made the code run much faster as we didn’t need to reference big object classes to modify one thing and made our messages way smaller. Finding what part of the code to change became also much easier as all we needed to know was what index was being modified, and change everything using that. This taught us a lot about how data oriented programming can be very useful to simplify code.
Tales of fortune is an online multiplayer game and therefore the entire gameplay framework is designed to work around that concept. The connection between server and client is that every player is a client communicating their messages to the server and back. This is done using the riptide networking solution.
The server is the global place where data is collected and then distributed to the clients in order to effectively sync anything from logic to animations. The server is responsible for time, handling the different states of the game, and performing all the gameplay logic (without any animations, just the logic). The server is basically the meeting point for all clients.
Every player is a client with an ID and their build is where all the heavy assets are located. To minimize data sent to the server we simply receive data from the server and act accordingly on all clients. The client's main responsibility is to retrieve all the gameplay data from the server and produce the visuals of it on your device. For example all the game animations happen only locally on the client.
Every single multiplayer logic happens through messages getting sent from either the server to the clients or a client sending a message to the server. Using the message ID riptide uses reflection to call functions when messages have been sent from either side using the message handler attribute, however the IDs must match where you create the message and where you retrieve it and all data needs to be collected in the correct order.
private void SetRoundState() { Message _message = Message.Create(MessageSendMode.reliable, ServerToClientId.sendRoundState); _message.AddString(roundState.ToString()); NetworkManager.Singleton.Server.SendToAll(_message); }
[MessageHandler((ushort)ServerToClientId.sendRoundState)] static void SetRoundState(Message message) { string roundState = message.GetString(); switch (roundState) { case "PlanningPhase": currentState = RoundState.PlanningPhase; break; case "PlayingPhase": currentState = RoundState.PlayingPhase; break; } changedRound = true; }
Players can join a game together through the matchmaking system implemented in the game. When the minimum required players needed for a match has been met the lobby owner can start the game.
For the multiplayer in-game chat we took mostly inspiration from League of Legends chat system. The chat shows allies as green and all enemies and foes as red. The chat also shows a timestamp for when the message was sent in game time. The client responsible for the message sends the message to the server, the server then resends that message to all other clients, using the ID each client checks if the message sent is local or not, that is how the client determines which message belongs to it and what messages belong to other clients (rendering them as enemies). The server also has the feature to send Server messages to clients rendering them as yellow. To prevent any client from crashing the server, all messages have a length limit and the server can start deleting messages if it exceeds a certain max value.
private static int chatLength = 0; [MessageHandler((ushort)ClientToServerId.sendChatText)] private static void UpdateChat(ushort fromClientId, Message message) { string text = message.GetString(); string timeText = "[" + TOFTimeHandler.GetTimeAsMinutes() + "]"; string time = timeText; string username = TOFPlayer.players[fromClientId].username; string chatMessage = text; Message _message = Message.Create(MessageSendMode.reliable, ServerToClientId.sendChatText); _message.AddUShort(fromClientId); _message.AddString(time); _message.AddString(username); _message.AddString(chatMessage); NetworkManager.Singleton.Server.SendToAll(_message); chatLength++; }