OOTetris Design
Contents |
Introduction
During my Bachelor studies in Germany I had to implement a network based multiplayer Tetris. It is possible to run the game with many players on different computers in a network. The game works quite well but the software design in general is quite messy. The Design we came up with was more or less guided by our progress on programming different parts of the game. So we started out with the Server-Client Communication. After the basic communication was achieved we started implementing the tetris game itself with its graphical user interface and its game logic (game control).
The last couple of days I was trying to figure out, what would be the best way to start redesigning and refactoring the current design. So I figured it might be smart to go over every part of the design in detail and explain how it works. I think that is a good starting point to find out what has to be done differently. I think just by describing different parts I will notice things I need to change. I think describing in detail how it works helps also other guys to give me feedback.
The old Design
Class-Diagram (package overview)
The old design consists out of 5 different packages as you can see in the UML-Diagram above:
- The green package in the UML-Diagram is responsible for the Server-Client communication.
- The pink package controls the game. Thats where the game intelligence is implemented.
- The blue package is obviously the graphical user interface.
- The red package contains the different stones of the tetris game.
- The yellow package is a customized list which is used to the store the stones of every client.
State-of-the-art Class-Diagram
To be able to understand the whole Design, I think it makes sense to have a look at the more detailed version of the Class-Diagram:
This diagram shows the whole structure of all classes and their relationships a bit more in detail.
Network Structure
As I said before this is a multi player tetris which can be played over the network. To be able to do that a client-server structure is necessary. As you can see in the diagram there is a GUI for the Client as well as for the Server. The Server GUI (S_GUI) gets started. Here the player chooses either to setup his/her computer as server or just as client. In case it is setup as server, the server structure as well as the client structure are built. In case it is just a client, only the client (C_GUI) is started. To understand the server-client communication I attached a diagram, which describes the essentials of the network communication.
So basically what does the diagram tell us. There is a server-sided Thread called Server_connectClient. This thread waits till all Clients are connected to it. In the server GUI the user defines how many players the game is going to have. As soon as all clients are connected the game starts. That means on the server side for every client there are two threads constructed, S_toClient and S_fromClient. S_toClient is responsible for sending stone- and command-objects to the client. On the client's side a thread Client_fromServer is constructed which handles the client communication. It receives the stone- and command-objects from the server and handles those in whatever way. Besides handling the receiving objects this thread can also send commands to the server (more detail about the different commands later on). So on the server side the thread S_fromClient treats the receiving commands. So in general the server has 2 threads for every single client to communicate with it. Every client has just one thread which handles the communication.
Commands
The command objects get sent over the network from the client to the server and vice versa. On the serve side, as said before, the thread S_fromClient is handling receiving command objects and the thread S_toClient is doing the sending of command objects to the client. Commands starting with an "S_" are sent from the server to the client; commands starting with a "C_" are sent from the client.
So far there is a command-package with 5 different command objects:
- S_printScoreCommand is sent from the server to the clients as soon as the score of players has changed and the list of players and their scores were updated/sorted, the server sends a S_printScoreCommand to the clients. By calling up the execute()-method the new score list is updated on the GUI.
- S_fallCommand is responsible for the falling/moving down of the stones on the gamefield. On every tick of the S_clock object a S_fallCommand is sent to all clients to have the same game pace for players.
- C_newScoreCommand is the counterpart of the S_printScoreCommand on the client side. As soon as a client removes lines and therefore gets new points the score list has to be updated. THat's when the client sends this command to the server.
- C_getStoneCommand requests a new stone. In the S_fallCommand the stone gets moved down. By doing that it is checked if there is a collision. As soon as there is a collision the stone gets transfered into the gamefield and a new stone has to be requested.
- C_gameOverCommand is sent when a client is game over.
As you can see there is an interface called CS_Command, which has two execute() methods, one for commands executed on the server side and one for the commands executed on the client side. There are two methods because they have different parameters. So all "C_"-commands just implement the execute(S_fromClient) method while all "S_"-commands just implement the execute(C_fromServer) method. This in Jason's and my understanding is a violation of the No fat interfaces principle because this interface is designed for two different aims. In this case there should probably either be two interfaces one for the server commands and one for the client commands or I think the more elegant solution would be to add an interface, for example called CSCommunication, which would be implemented by the server-client-threads, S_fromClient, S_toClient and Client_fromServer. Thus you wouldn't need two execute()-methods anymore. You would end up with one method execute(CSCommunication).
Also so far when the client sends the C_getStoneCommand the server sends the client just one stone-object. The server has a list of stones called SList. This list consists of SElements which contain stone objects. The stone objects are randomly created. To assure that every client gets the same stones and in particular in the same order the list memorizes which clients has requested which SElement's stone. So every client has basically just 2 stones, one on the gamefield and the next stone in the preview field. According to that when the client asks for a new stone by sending the C_getStoneCommand, the server sends a specific stone object back to client. To receive the different objects sent by the server the Client_fromServer thread has to check from which type those objects are:
Object serverObjectOutput = objectFromServer.readObject(); if (serverObjectOutput instanceof S_Stone) { knowsAll.actualStone = (S_Stone)serverObjectOutput; doSomething(); } else if (serverObjectOutput instanceof CS_Command) { CS_Command commandObject = (CS_Command)serverObjectOutput; commandObject.execute(this); }
So first the object gets checked of which type it is (instance of) and then has to be downcasted. This clearly violates the Avoid downcasting principle. So it would be way nicer to just have commands getting send over the network. All the receiver has to do is read the received object and execute it:
CS_Command commandObject = (CS_Command)objectFromServer.readObject(); commandObject.execute(this);
So I am going to add a new command class. This command will send stone objects to the clients. To simplify the whole organization of keeping track of the stones, every client will have its own list of stones. Thus instead of saving the stones on the server side, every client is going to do that job from now on on its own. The stones are just created on the server to make sure that every clients gets the same stones in the same order. The new command, S_newStonesCommand, contains an array of 5 randomly created stone objects and gets sent to all clients. They just have to add the incoming stones to their stone list. As soon as one of the clients' list gets smaller than 3 a C_getStoneCommand is sent to request new stones. This simplifies a lot.
Furthermore the command package with its interface and the concrete classes realizes the Strategy design pattern. However, I just found out it could also be a Command patter. I am not totally sure.
S_Common & C_Common
To be able to have access to all attributes over different threads I used two classes, S_Common and C_Common. Both are mainly responsible for keeping track of different data fields, whereas S_Common is doing that on the server side, C_Common is doing it on the client side. There are a couple of arguments which speak against the use of those two classes. I think the main reason why you shouldn't do that is because you should keep related data and behavior in one place. Since both classes pretty much don't have any behavior it doesn't make sense to store those attributes in there. Besides because of those classes call up operation can have really nasty nesting that clearly disagrees with the Law of demeter and Tell, don't ask:
cCommon.objectToServer.writeObject(new C_gameOverCommand());
Therefore in a first step of the refactoring all attribute by using the Move Field refactor method will be transfered to the objects they belong to. In most cases thez will be moved into the client-server threads.
Constructionsites
- Tetrisfield and stones because of Switch statement smell
- class names
- Extract class out of Client_fromServer
- adding game ideas (sendLines/no timelimit)
- GUI classes refactoring