Introduction
Drench is a single-player game originally developed using Adobe Flash (try Googling for "the world's simplest Flash game"). It's fairly popular and has already been ported to the Android platform. There are many game clones available at Google Play and Amazon app stores, including Coloroid, PaintIT, Splash! and Floodfill, to name a few.
Although the exact game rules may differ, all of these games are single-player. Zyan Drench is an attempt to adapt the game for two players and introduces two new gameplay modes: playing against the computer and network game mode. This article describes building the Android version of the game using C# language, Xamarin.Android platform (Indie edition) and Zyan Communication Framework for network gaming.
Game Overview
Drench is a puzzle game that is very easy to grasp, but a bit difficult to explain. The game starts with a 15x15 board of random blocks, or pixels. Starting from the top-left pixel, you have to fill (drench) the whole board with one color. You do this by setting the top-left pixel to a new color. When you change the color of the pixel into the same color of the adjacent pixels, you expand the drenched area:
The original Drench game is single-player, with a 15x15 board and a limit of thirty moves per level. Several game implementations allow selecting different board sizes, the limit is typically twice the board size, and most implementations use 6 colors. Our single-player game will use the same parameters to be backward-compatible.
Two-player Mode
Adapting the game for two players is straightforward: if the first player starts from the top-left pixel, then the opponent takes the opposite corner. The game is played turn by turn sequentially until the whole board is painted in two colors. Player who drenched more pixels than his opponent wins the game:
Forbidden Colors
In a single-player mode, using the same color twice makes no sense because all adjacent pixels of that color are already captured. Two player mode adds one more restriction: you cannot eat up your opponent's pixels, therefore you cannot use the same color as your opponent. On each turn, there are two colors that cannot be used: it's your current color and the color of your opponent. Let's call them forbidden colors.
Game Development
To model the game, we need a board, which is a two-dimensional array of pixels. Every pixel has a color, which we can encode using an integer number from 0 to 5. To display the board, we need to assign any distinct colors to these numbers (i.e., create a palette):
public class DrenchGame
{
public const int BoardSize = 15;
public const int ColorCount = 6;
private int[,] Board = new int[BoardSize, BoardSize];
private Color[] Palette = new[] { Color.Red, Color.Green, ... };
public void NewGame()
{
}
public void MakeMove(int color)
{
}
public void CheckIfStopped()
{
}
}
Drench game has quite a few different gameplay modes: single-player, double-player, playing against the computer (with several skill levels) and network-based. All of them share the same rules of working with the board: setting new color, expanding the drenched area, randomizing the board for a new game, etc. Let's extract all these details to a separate class representing a board.
DrenchBoard
Below is a class that we'll use to represent a board. Like a computer screen, a pixel location is determined by its X and Y coordinates, where (0, 0) is the top-left corner. An indexer is used to access the colors of individual pixels: this[x, y]. For the convenience, we'll wrap the coordinates (just like array indexes in perl language) so that this[-1, -1] means the same as this[BoardSize - 1
, BoardSize - 1
].
The board will carry out all the calculations needed to expand the drenched area. Each player tries to drench the board starting from its own location, that's why method SetColor(x, y, color)
takes x
and y
coordinates. The exact algorithm for SetColor
is discussed below:
public class DrenchBoard
{
public void Randomize()
{
}
public void SetColor(int x, int y, int color)
{
}
public bool CheckAllColors(param int[] allowedColors)
{
}
public int this[int x, int y]
{
get
{
if (x < 0)
x = BoardSize - x;
if (y < 0)
y = BoardSize - y;
return Board[x, y];
}
}
}
To represent a single location within a board, I created the Point
structure: new Point(x, y)
. This structure is used by the flood fill algorithm. The algorithm operates with sets of points, and to optimize comparisons Point
structure implements the IEquatable<point> </point>
<point>interface:
public struct Point: IEquatable<Point>
{
private int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
...
}
Flood Fill Algorithm
I'm not an expert in computer graphics, but flood fill doesn't look like a difficult thing to do, so I just made up my own algorithm that works as follows. For every pixel, I examine its four neighbours, and remember those having the same color. Repeating the process recursively, I end up with a finite set of adjacent pixels of the same color as the starting pixel. Finally, I iterate over these pixels setting all of them to the new color.
While implementing this simple algorithm, I converted recursion into iteration to consume less stack memory and used a Queue of points scheduled for processing. But as it turned out, Queue
class behaves quite slowly when processing large amounts of pixels.
Then I realized that the order or processing pixels is not important at all, and replaced a Queue
with a HashSet
. That magically fixed all the performance issues! HashSet
performance doesn't depend on the set size, so it handles hundreds of items just as fast as a few ones. Below is the complete flood fill algorithm I ended up with:
public void SetColor(int x, int y, int newColor)
{
var color = Board[x, y];
if (color == newColor)
{
return 1;
}
var points = new HashSet<Point>();
var queue = new HashSet<Point>();
queue.Add(new Point(x, y));
var adjacents = new[] { new Point(-1, 0), new Point(0, -1), new Point(0, 1), new Point(1, 0) };
while (queue.Any())
{
var point = queue.First();
queue.Remove(point);
points.Add(point);
Board[point.X, point.Y] = newColor;
foreach (var delta in adjacents)
{
var newX = point.X + delta.X;
var newY = point.Y + delta.Y;
if (newX < 0 || newX > BoardSize - 1 || newY < 0 || newY > BoardSize - 1)
{
continue;
}
if (Board[newX, newY] != color)
{
continue;
}
var newPoint = new Point(newX, newY);
if (points.Contains(newPoint))
{
continue;
}
queue.Add(newPoint);
}
}
}
Using the provided DrenchBoard
class for game programming is very straightforward:
class SomeKindOfDrenchGame
{
public void NewGame()
{
Board.Randomize();
}
public void MakeMove(int newColor)
{
Board.SetColor(0, 0, newColor);
}
}
IDrenchGame Interface
Following the DRY (Don't Repeat Yourself) principle, we'd like our application to handle all game modes using the same UI which looks like a pixel board with a few colored buttons below it:
Player makes a move by touching the colored buttons. Buttons of forbidden colors are disabled. As the game proceeds, the UI updates the current status text above the board. This is common for all game modes, so we can describe all that as an interface. Actual game interface may be a bit more complex, but we can always add more methods and properties as we need:
public interface IDrenchGame
{
DrenchBoard Board { get; }
void NewGame();
void MakeMove(int color);
bool IsStopped { get; }
string CurrentStatus { get; }
IEnumerable<int> ForbiddenColors { get; }
event EventHandler GameChanged;
event EventHandler GameStopped;
}
To make things even simpler, we'll create a base abstract
class for all game modes. Descendant classes will override MakeMove
and CheckIfStopped
methods according to the game rules:
public abstract class DrenchGameBase
{
public virtual DrenchBoard Board { get; private set; }
public virtual void NewGame()
{
Board.Randomize();
}
public virtual void SetColor(int x, int y, int color)
{
Board.SetColor(x, y, color);
OnGameChanged();
}
public abstract MakeMove(int color);
protected abstract CheckIfStopped();
public virtual bool IsStopped { get; protected set; }
public virtual string CurrentStatus { get; protected set; }
public virtual IEnumerable<int> ForbiddenColors { get; protected set; }
public event EvenHandler GameChanged;
protected void OnGameChanged()
{
var gameChanged = GameChanged;
if (gameChanged != null)
gameChanged(this, EventArgs.Empty);
}
public static IEnumerable<int> Enumerate(params int[] colors)
{
return colors;
}
}
SinglePlayerGame and TwoPlayerGame
Using the provided DrenchGameBase
class, creating specific game modes is very easy. Overriding MakeMove
and CheckIfStopped
methods, we can control how the game is going on. Base class carries out all the calculations using the Board
instance. Here is the complete source code for the single-player game:
public class SinglePlayerGame : DrenchGameBase
{
public const int MaxMoves = 30;
public override void NewGame()
{
base.NewGame();
CurrentMove = 1;
ForbiddenColors = Enumerate(Board[0, 0]);
CurrentStatus = string.Format("{0} moves left. Good luck!", MaxMoves);
OnGameChanged();
}
public override void MakeMove(int value)
{
CurrentMove++;
CurrentStatus = string.Format("Move {0} out of {1}", CurrentMove, MaxMoves);
ForbiddenColors = Enumerable.Repeat(value, 1);
SetColor(0, 0, value);
}
protected override void CheckIfStopped()
{
var allowedColor = Board[0, 0];
var success = Board.CheckAllColors(allowedColor);
if (success || CurrentMove > MaxMoves)
{
var result = success ? "won" : "lost";
OnGameStopped(true, "You have {0} the game!", result);
}
}
}
TwoPlayerGame
keeps track of the current player so that each call to MakeMove
paints over the top-level or bottom-right pixel alternatively. CheckIfStopped
checks if all the pixels have one of the two colors.
Assembling an Android Application
Let's use the provided game classes to build a working Android application. A typical application consists of several activities (screens) that interact with the user. Each activity contains several views combined into a hierarchy to create a user interface. I won't dive into much details of Android application structure because there are more than plenty good articles already available on the subject, so I can just focus on some details.
Our main game activity will use TableLayout
to create the board and the set of buttons. Buttons are created in the layout designer, and the board is built programmatically, so that's easy to change the board size on-the-fly. The layout for the board screen looks like this (most of the details skipped):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TableLayout android:id="@+id/boardTable"
android:stretchColumns="*">
<TableRow android:id="@+id/tableRow0">
</TableRow>
</TableLayout>
<TableLayout android:id="@+id/buttonsTable"
android:stretchColumns="*">
<TableRow android:id="@+id/tableRow1">
<Button android:id="@+id/button0"/>
<Button android:id="@+id/button1"/>
<Button android:id="@+id/button2"/>
</TableRow>
<TableRow android:id="@+id/tableRow2">
<Button android:id="@+id/button3"/>
<Button android:id="@+id/button4"/>
<Button android:id="@+id/button5"/>
</TableRow>
</TableLayout>
</LinearLayout>
And here is the code that populates the board table with blocks, executed in OnCreate
method:
var colors = Palette;
for (var j = 0; j < BoardSize; j++)
{
tableRow = new TableRow(BaseContext);
tableRow.LayoutParameters = new TableLayout.LayoutParams(
TableLayout.LayoutParams.WrapContent,
TableLayout.LayoutParams.WrapContent, 1f);
table.AddView(tableRow);
for (var i = 0; i < BoardSize; i++)
{
var button = new Button(BaseContext);
button.LayoutParameters = new TableRow.LayoutParams(i);
button.LayoutParameters.Width = 1;
button.LayoutParameters.Height = ViewGroup.LayoutParams.MatchParent;
button.SetBackgroundColor(colors[(i + j * 2) % 6]);
tableRow.AddView(button);
Tiles[i, j] = button;
}
}
Each block is represented by a Button view. Creating rows with equal gravity values ensures that all rows have same height, and setting android:stretchColumns="*"
makes columns of same width, exactly what we need for the pixel board.
Note: Android devices support different screen sizes and width to height ratios, that's why blocks may not always be perfect squares.
Handling Device Rotation
In Android, Activity
objects can be created and destroyed every time Android feels like doing so. For example, when you rotate the device, the current activity is recreated from scratch, and you have to load the layout and re-create the board. This means that you cannot simply store the current game instance in the activity. The current game instance has to be put elsewhere.
Custom Application Class
Looks like the easiest way to store it safely is to create a custom application class. Application
instance exists for the whole lifetime of the process, and it's available to all activities through the Application
property. The only gotcha to be aware of is that application class is created from Java code, so it has to be a special constructor that looks like this:
public CustomApplication(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
All instances that I need to share across different activities can be published as properties of the application class:
public IDrenchGame DrenchGame { get; set; }
Accessing the application instance from the activities looks like this:
private CustomApplication App { get { return (CustomApplication)Application; } }
...
var currentGame = App.DrenchGame;
Game
instance interacts with the board activity using events such as GameChanged
and GameStopped
. Activity
subscribes to these events in OnResume
and unsubsribes
from them in OnPause
method:
protected override void OnResume()
{
base.OnResume();
DrenchGame.GameChanged += UpdateTiles;
DrenchGame.GameStopped += StopGame;
}
protected override void OnPause()
{
base.OnPause();
DrenchGame.GameChanged -= UpdateTiles;
DrenchGame.GameStopped -= StopGame;
}
Unsubscribing from game events is very important: active event handler will prevent the activity instance from being garbage-collected and will create a memory leak.
Starting a Game and Displaying a Board Activity
When a game is created, the only thing left to do is to start the activity which will interact with the user.
App.DrenchGame = new SinglePlayerGame();
StartActivity(typeof(DrenchBoardActivity));
We can create main menu with different options: start single-player game, two players game, play against Android, etc. Every handler for our menu items will work the same way with the only difference in game classes being created.
Adding Network Support
Multiplayer network game mode requires special synchronization between remote players. For example, both players should have the same board to play with. The game cannot start unless both players are ready to play. The game cannot proceed if one of players has quit the game, and so on. Our IDrenchGame
interface is not enough to handle all of that: we'll need extra methods and events.
To save the network bandwidth, we won't send the whole game state across the wire on each turn. Instead, every party will maintain its own instance of DrenchBoard
, and we'll only send lightweight events for each move and game status update.
IDrenchGameServer Interface
There is no point in adding new members to IDrenchGame
interface. Network-specific methods and events make no sense for the local games. Instead, let's introduce a new interface to interoperate with the game server, extending IDrenchGame
:
public interface IDrenchGameServer : IDrenchGame
{
void Join();
void Leave();
bool IsReady { get; }
event EventHandler GameStarted;
event EventHandler<MoveEventArgs> Moved;
}
Working with this interface assumes the following protocol:
- Establish a connection with
IDrenchGameServer
- Subscribe to events
GameStarted
and Moved
- Call
Join
method to begin a new game - Call
Move
method to make your move - Handle
Moved
event to react to your opponent's move - Handle
GameStopped
event (inherited from IDrenchGame
) to stop the current game - Call
NewGame
(also inherited from IDrehchGame
) to begin a new game - If you want to abort the current game, call
Leave
method so the server can stop - Unsubscribe from server events
- Disconnect from server
DrenchGameServer and DrenchGameClient
Let's create two special game classes that implement the protocol listed above. For these classes, I decided to reuse my TwoPlayerGame
class that already implements everything needed for the two-player game mode.
Both of my classes use a private
instance of the TwoPlayerGame
to manage the game state locally. For example, IsStopped
and CurrentStatus
properties are directly taken from the InnerGame
instance:
public class DrenchGameServer : DrenchGameBase, IDrenchGameServer
{
public DrenchGameServer()
{
InnerGame = new TwoPlayerGame();
}
private TwoPlayerGame InnerGame { get; private set; }
public override bool IsStopped
{
get { return InnerGame.IsStopped; }
protected set { ... }
}
public override string CurrentStatus
{
get { return InnerGame.CurrentStatus; }
protected set { ... }
}
}
Implementing server-specific methods Join
and Leave
is very easy. All we need to do is to make sure that Join
method can only be called once (we always play against single opponent):
public void Join()
{
if (IsReady)
{
throw new InvalidOperationException("Second player " +
"already joined the game. Try another server.");
}
IsReady = true;
OnGameStarted();
}
public void Leave()
{
IsReady = false;
IsStopped = true;
OnGameStopped(false, "Second player has left the game.");
}
DrenchGameClient
class in addition to the local innerGame
instance holds a reference to a remote IDrenchGameServer
. It connects to the server, copies the board data, subscribes to server events and calls the Join
method:
public class DrenchGameClient : DrenchGameBase
{
public DrenchGameClient(IDrenchGameServer server)
{
Server = server;
InnerGame.Board.CopyFromFlipped(Server.Board);
InnerGame.SkipMove();
UpdateStatus();
JoinServer();
}
public async void JoinServer()
{
await Task.Factory.StartNew(() =>
{
Server.GameStarted += ServerGameStarted;
Server.GameStopped += ServerGameStopped;
Server.Moved += ServerMoved;
Server.Join();
});
}
...
}
Note that JoinServer
method is asynchronous. Remote calls are thousand times slower than local calls due to network latency. To make sure that our game doesn't freeze the UI, we need to perform remote calls asynchronously. Please also note that the stable branch of Xamarin.Android
still doesn't support async/await pattern, so you'll need the most recent beta version of the framework to compile this code.
Another point of interest is how DrenchGameClient
makes moves. The only thing it does is calling server's method and handling server's events. Game
client doesn't change the state of its InnerGame
: it's completely controlled by the remote server. Note that MakeMove
method is also asynchronous because it involves a remote call:
public override async void MakeMove(int value)
{
await Task.Factory.StartNew(() => Server.MakeMove(value));
}
private void ServerMoved(object sender, MoveEventArgs e)
{
InnerGame.MakeMove(e.Color);
UpdateStatus();
}
Hosting DrenchGameServer
To share game server over network, we'll use Zyan Communication Framework. This library doesn't require any extra treatment for our classes, so we can just publish DrenchGameServer
instance as it is. The diagram below outlines the typical architecture of Zyan application (note however that these internals won't show up in our application code except for ZyanComponentHost
and ZyanConnection
classes). Cyan boxes represent Zyan
library classes, and yellow boxes stand for the application code:
To start the server, we need to create a ZyanComponentHost
instance with a TCP protocol. Game server as well as zyan host instances will be stored in properties of our custom application class, just like other shared instances:
public void StartServer()
{
if (ZyanHost == null)
{
var portNumber = Settings.PortNumber;
var authProvider = new NullAuthenticationProvider();
var useEncryption = false;
var protocol = new TcpDuplexServerProtocolSetup(portNumber, authProvider, useEncryption);
ZyanHost = new ZyanComponentHost(Settings.ZyanHostName, protocol);
}
var server = new DrenchGameServer();
DrenchGame = server;
ZyanHost.RegisterComponent<IDrenchGameServer, DrenchGameServer>(server);
}
Note: The only transport protocol currently available in Android version of Zyan
library is duplex TCP protocol.
Connecting to DrenchGameServer
To connect to the game server published by Zyan component host, the following steps are needed:
- Establish a connection by creating a
ZyanConnection
class - Create a proxy for the remote
IDrenchGameServer
- Create a
DrenchGameClient
, passing server proxy as the constructor parameter
Let's add a method to our custom application class:
public IDrenchGameServer ConnectToServer(string hostName)
{
var protocol = new TcpDuplexClientProtocolSetup(encryption: false);
var url = protocol.FormatUrl(host, Settings.PortNumber, Settings.ZyanHostName);
var zyanConnection = new ZyanConnection(url, protocol);
return zyanConnection.CreateProxy<IDrenchGameServer>();
}
...
var server = await Task.Factory.StartNew(() => App.ConnectToServer(Settings.ServerAddress));
App.DrenchGame = new DrenchGameClient(server);
StartActivity(typeof(DrenchBoardActivity));
So, we've just created a simple multiplayer game for Android with Wifi support. Although this article doesn't cover every little aspect of the application, I hope it shows all the important points. Any feedback would be greatly appreciated!
P.S. The game has been uploaded to the Google Play app store, and the most recent source code for it is available at Codeplex and github.
References
History