Click here to Skip to main content
15,867,453 members
Articles / Web Development / HTML

Zyan Drench, A Game for Android with Wifi Support

Rate me:
Please Sign up or sign in to vote.
4.99/5 (17 votes)
28 Feb 2016MIT12 min read 46.5K   1.3K   37   9
This article describes building an Android game with networking support using C#, Xamarin.Android platform and Zyan Communication Framework.

Screenshots

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:

Single-player mode

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:

Two-player mode

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):

C#
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()
    {
        // randomize the board
    }

    public void MakeMove(int color)
    {
        // flood fill the board starting from the top-left pixel
    }

    public void CheckIfStopped()
    {
        // check if the whole board is drenched
    }
}

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:

C#
public class DrenchBoard
{
    // skipped: BoardSize, ColorCount and Board (same as above)

    public void Randomize()
    {
        // assign a random color to every pixel of the board
    }

    public void SetColor(int x, int y, int color)
    {
        // flood fill algorithm (discussed below)
    }

    public bool CheckAllColors(param int[] allowedColors)
    {
        // check if all pixels have one of the allowed colors  
    }

    public int this[int x, int y]
    {
        get
        {
            // wrap coordinate values so that Board[-1, -1]
            // means the right-bottom corner
            if (x < 0)
                x = BoardSize - x;
            if (y < 0)
                y = BoardSize - y;

            // return the color of a pixel given its coordinates
            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:

C#
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:

C#
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;

        // process adjacent points of the same color
        foreach (var delta in adjacents)
        {
            var newX = point.X + delta.X;
            var newY = point.Y + delta.Y;

            // skip invalid point
            if (newX < 0 || newX > BoardSize - 1 || newY < 0 || newY > BoardSize - 1)
            {
                continue;
            }

            // skip pixels of other colors
            if (Board[newX, newY] != color)
            {
                continue;
            }

            // skip already processed point
            var newPoint = new Point(newX, newY);
            if (points.Contains(newPoint))
            {
                continue;
            }

            // schedule the point for processing
            queue.Add(newPoint);
        }
    }
}

Using the provided DrenchBoard class for game programming is very straightforward:

C#
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:

Screenshot

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:

C#
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:

C#
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)
    {
        // utility method to return an IEnumerable<int>
        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:

C#
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);

        // set the new color
        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):

XML
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Stub view for the board -->
    <TableLayout android:id="@+id/boardTable"
        android:stretchColumns="*">
        <TableRow android:id="@+id/tableRow0">
        </TableRow>
    </TableLayout>
    <!-- Buttons panel -->
    <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:

C#
// Create board tiles
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:

C#
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:

C#
public IDrenchGame DrenchGame { get; set; }

Accessing the application instance from the activities looks like this:

C#
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:

C#
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.

C#
App.DrenchGame = new SinglePlayerGame(); // or any other game class!
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:

C#
public interface IDrenchGameServer : IDrenchGame
{
    void Join(); // join the remote game

    void Leave(); // leave the remote game

    bool IsReady { get; } // both players are ready

    event EventHandler GameStarted;

    event EventHandler<MoveEventArgs> Moved; // other player made a move
}

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:

C#
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):

C#
public void Join()
{
    if (IsReady)
    {
        // this exception will travel across the wire to the client
        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:

C#
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:

C#
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:

Zyan architecture

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:

C#
public void StartServer()
{
    if (ZyanHost == null)
    {
        // set up duplex tcp protocol, no security required
        var portNumber = Settings.PortNumber;
        var authProvider = new NullAuthenticationProvider();
        var useEncryption = false;
        var protocol = new TcpDuplexServerProtocolSetup(portNumber, authProvider, useEncryption);

        // start the server
        ZyanHost = new ZyanComponentHost(Settings.ZyanHostName, protocol);
    }

    // create server game
    var server = new DrenchGameServer();
    DrenchGame = server;

    // register game component, so a client can connect to the 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:

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

// this method is used as follows (note the non-blocking asynchronous call):
var server = await Task.Factory.StartNew(() => App.ConnectToServer(Settings.ServerAddress));

// and now -- already familiar code to start a game:
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.

Get it on GooglePlay QR code

References

History

  • 05.08.2013 Initial post

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior) ULTIMA Businessware
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionImages are broken Pin
kellyapt121-Dec-15 14:53
kellyapt121-Dec-15 14:53 
AnswerRe: Images are broken Pin
AlexeyYakovlev28-Feb-16 12:14
professionalAlexeyYakovlev28-Feb-16 12:14 
GeneralHighly appropriated work Pin
Jennifer star6-Feb-15 20:04
Jennifer star6-Feb-15 20:04 
i like very much playing games and also i am also an android developer, i have also developed many games but this time i will try this code to develop a game such like that.
you could find my work on Android apps.
GeneralMy vote of 5 Pin
Afzaal Ahmad Zeeshan18-Oct-14 21:03
professionalAfzaal Ahmad Zeeshan18-Oct-14 21:03 
GeneralRe: My vote of 5 Pin
AlexeyYakovlev20-Oct-14 1:48
professionalAlexeyYakovlev20-Oct-14 1:48 
GeneralMy vote of 5 Pin
ThatsAlok6-Sep-13 18:41
ThatsAlok6-Sep-13 18:41 
GeneralRe: My vote of 5 Pin
AlexeyYakovlev6-Sep-13 19:32
professionalAlexeyYakovlev6-Sep-13 19:32 
GeneralMy vote of 5 Pin
ridoy4-Aug-13 21:04
professionalridoy4-Aug-13 21:04 
GeneralRe: My vote of 5 Pin
AlexeyYakovlev4-Aug-13 21:18
professionalAlexeyYakovlev4-Aug-13 21:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.