Let’s create a mobile game with Xamarin and OpenGL – Part 6 (TicTacToe)

In this part we will finally create a fully playable game! TicTacToe is the perfect game to start with, because everybody knows it and it’s easy to implement.

Creating a game

Of course now we need to implement the logic for our game. A very good way to do this is by separating the game code totally from the rendering code. Mixing game and rendering logic will make your code very hard to read and hard to maintain. Create a new class “TicTacToe.cs” in “MyGame.Shared”:

namespace MyGame.Shared { public class TicTacToe { public enum Player { Cross, Circle } public Player?[][] Playground { get; private set; } = { new Player?[]{ null, null, null }, new Player?[]{ null, null, null }, new Player?[]{ null, null, null } }; public Player NextTurn { get; private set; } = Player.Cross; public int SelectedTiles { get; private set; } public int TilesX { get { return Playground.Length; } } public int TilesY { get { return Playground[0].Length; } } public int TotalTiles { get { return TilesX * TilesY; } } public Player? Winner { get; private set; } private void ClearPlayground() { for (int y = 0; y < TilesY; y++) { for (int x = 0; x < TilesX; x++) { Playground[x][y] = null; } } } public void Select(int tileX, int tileY) { // When a round was won, reset the game on the next touch if (Winner.HasValue) { Reset(); return; } // If the touch position is valid check if there is a winner or the game is over. // Otherwise determine the player who has the next turn. if (tileX >= 0 && tileX < TilesX && tileY >= 0 && tileY < TilesY) { if (!Playground[tileX][tileY].HasValue) { SelectedTiles++; Playground[tileX][tileY] = NextTurn; if (IsWinner(NextTurn, tileX, tileY)) { Winner = NextTurn; return; } NextTurn = NextTurn == Player.Cross ? Player.Circle : Player.Cross; if (SelectedTiles >= TotalTiles) { Reset(); } } } } private bool IsWinner(Player player, int tileX, int tileY) { // Check if player has full horizontal line for (int x = 0; x < TilesX; x++) { if (Playground[x][tileY] != player) { break; } if (x == TilesX - 1) { return true; } } // Check if player has full vertical line for (int y = 0; y < TilesY; y++) { if (Playground[tileX][y] != player) { break; } if (y == TilesY - 1) { return true; } } // Check if player has full diagonal line from bottom left to top right for (int d = 0; d < TilesY; d++) { if (Playground[d][d] != player) { break; } if (d == TilesY - 1) { return true; } } // Check if player has full diagonal line from top left to bottom right for (int d = 0; d < TilesY; d++) { if (Playground[d][TilesY - 1 - d] != player) { break; } if (d == TilesY - 1) { return true; } } return false; } private void Reset() { ClearPlayground(); NextTurn = Player.Cross; SelectedTiles = 0; Winner = null; } } }

As you can see we are not using anything from the engine. Also we do not think in pixels here, but only in plain game logic. The central method of this game class is “Select”. Whenever the user touches the screen this method will be called and determines what happens in the game. The first touch will let the player cross make the first move. The next time it will be circle and so on. Each time it will also check if the game is over or if there is a winner. When there is a winner the next touch will reset the game so that a next round can be started. The rest of the code is very self-explanatory.

Rendering the game

Now we have to adapt the scene so that it will display the state of the game class:

using Engine.Shared; using Engine.Shared.Abstractions; using Engine.Shared.Drawables; using Engine.Shared.Managers; using Engine.Shared.Renderers; using OpenTK; using System.Reflection; namespace MyGame.Shared { public class GameScene : IScene { private readonly GameController gameController; private readonly InputManager inputManager; private readonly RectangleRenderer rectangleRenderer; private readonly SpriteRenderer spriteRenderer; private readonly TextureManager textureManager; private readonly BufferManager bufferManager; private DrawableRectangle rectangle; private DrawableSprite cross; private DrawableSprite circle; private DrawableSprite crossWins; private DrawableSprite circleWins; private float itemSize; private float itemsMarginX; private float itemsMarginY; private readonly Vector4 lightGray = new Vector4(0.8f, 0.8f, 0.8f, 1f); private readonly Vector4 darkGray = new Vector4(0.6f, 0.6f, 0.6f, 1f); private readonly Vector4 red = new Vector4(0.8f, 0f, 0f, 1f); private readonly Vector4 green = new Vector4(0f, 0.8f, 0f, 1f); private readonly TicTacToe ticTacToe = new TicTacToe(); public GameScene(GameController gameController, InputManager inputManager, RectangleRenderer rectangleRenderer, SpriteRenderer spriteRenderer, TextureManager textureManager, BufferManager bufferManager) { this.gameController = gameController; this.inputManager = inputManager; this.rectangleRenderer = rectangleRenderer; this.spriteRenderer = spriteRenderer; this.textureManager = textureManager; this.bufferManager = bufferManager; } public void Load() { var gameAssembly = Assembly.GetExecutingAssembly(); bool isPortrait = gameController.Width < gameController.Height; itemSize = isPortrait ? gameController.Width / ticTacToe.TilesX : gameController.Height / ticTacToe.TilesY; itemsMarginX = !isPortrait ? (gameController.Width - itemSize * ticTacToe.TilesX) / 2f : 0f; itemsMarginY = isPortrait ? (gameController.Height - itemSize * ticTacToe.TilesY) / 2f : 0f; rectangle = rectangleRenderer.Create(itemSize, itemSize); cross = spriteRenderer.Create(textureManager.Load(gameAssembly, "Resources.Images.Cross.png"), itemSize, itemSize); circle = spriteRenderer.Create(textureManager.Load(gameAssembly, "Resources.Images.Circle.png"), itemSize, itemSize); crossWins = spriteRenderer.Create(textureManager.Load(gameAssembly, "Resources.Images.Cross_Wins.png"), itemSize * 2, itemSize / 2f); circleWins = spriteRenderer.Create(textureManager.Load(gameAssembly, "Resources.Images.Circle_Wins.png"), itemSize * 2, itemSize / 2f); inputManager.Listeners.Add(new SelectionInputListener(this)); } public void Render(Matrix4 viewProjection) { if (ticTacToe.Winner.HasValue) { // Render win message spriteRenderer.BeforeRender(); var sprite = ticTacToe.Winner.Value == TicTacToe.Player.Cross ? crossWins : circleWins; spriteRenderer.Render(sprite, ticTacToe.Winner.Value == TicTacToe.Player.Cross ? red : green, Matrix4.CreateTranslation((gameController.Width - sprite.Width) / 2f, (gameController.Height - sprite.Height) / 2f, 0f) * viewProjection); spriteRenderer.AfterRender(); } else { // Render playground rectangleRenderer.BeforeRender(); for (int y = 0; y < ticTacToe.TilesY; y++) { for (int x = 0; x < ticTacToe.TilesX; x++) { rectangleRenderer.Render(rectangle, (y + x) % 2 == 0 ? lightGray : darkGray, Matrix4.CreateTranslation(x * itemSize + itemsMarginX, y * itemSize + itemsMarginY, 0f) * viewProjection); } } rectangleRenderer.AfterRender(); // Render crosses and circles spriteRenderer.BeforeRender(); for (int y = 0; y < ticTacToe.TilesY; y++) { for (int x = 0; x < ticTacToe.TilesX; x++) { var player = ticTacToe.Playground[x][y]; if (player.HasValue) { spriteRenderer.Render(player.Value == TicTacToe.Player.Cross ? cross : circle, player.Value == TicTacToe.Player.Cross ? red : green, Matrix4.CreateTranslation(x * itemSize + itemsMarginX, y * itemSize + itemsMarginY, 0f) * viewProjection); } } } spriteRenderer.AfterRender(); } } public void Select(float x, float y) { float positionX = (x - itemsMarginX) / itemSize; float positionY = (y - itemsMarginY) / itemSize; int tileX = positionX > 0 ? (int)positionX : -1; int tileY = positionY > 0 ? (int)positionY : -1; ticTacToe.Select(tileX, tileY); } public void Dispose() { textureManager.Dispose(); bufferManager.Dispose(); inputManager.Listeners.Clear(); } } }

First we will need to add new images. These contain the cross, the circle and a win message for both.

In the “Load” method these images are then loaded into sprites. Since their color is white, we can easily change the colors with our render methods. Also all dimension are calculated in respect the device resolution. The item size is calculated so that 3 items will fill the whole screen in portrait, or the whole height in landscape mode. Also the margins are calculated so that for both screen orientations, the playground can be displayed in the center of the screen. Whenever the orientation of the device is changed, the “Load” method will be executed again. Therefor it’s important that we calculate everything for the current orientation.

In the “Render” method we will render a win message if there is already a winner. Otherwise we will render a playground with 3 * 3 tiles. The tiles next to each other will always have an alternating color. On top of the playground the crosses and circles are rendered. Crosses will be rendered in red, circles in green.

You might wonder why we implemented all the “IDisposable” interfaces. As mentioned before when you for example change the orientation, the application will be reinstantiated again and everything from “MainActivity.OnCreate” will execute again. But in this case, opengl will be notified that all resources for this application can be disposed. The manual disposal therefor is only necessary if we want to switch between scenes and we want to release the device from unused memory. In games with bigger dimensions it might also make sense to release memory in the same scenes.

When you start the application you will be able to play tic tac toe with 2 players. Feel free to make it playable against an NPC or add more tiles to make it more difficult 🙂

You can download the source code of this article here: