【Android】MVVMでtic-toc-toeゲームを作成する

はじめに

以下を経緯としてMVVMを使用してtic-toc-toeゲームを作成します。

MVVMとは

MVVMは以下で構成されている

Model

  • データ、状態、ビジネスロジックを担当する
  • ビューやコントローラとは関連付けられておらず、再利用可能となる

View

  • View Modelによって公開される使用可能な変数やアクションにバインドする
  • 複数のViewを1つのView Modelにバインドできる

View Model

  • モデルをラップし、ビューが必要とするデータを準備する
  • ビューからモデルへデータを渡す橋渡しにもなる
  • View ModelからはViewが見えない

アプリケーションの作成

Model

今回の場合、Modelで定義するのは以下の要素です。

  • Player
  • Cell
  • Game

共通処理

共通で使用する処理を書いておきます。 引数の文字列がnullまたは空のときにtrueを返す関数です。 utilities/StringUtility.javaを作成します。

public class StringUtility {

   public static boolean isNullOrEmpty(String value) {
       return value == null || value.length() == 0;
  }
}

Player

プレイヤーはプロパティとして名前と値(○か✕か)を持つ model/Player.javaを作成します。

public class Player {

   public String name;
   public String value;

   public Player(String name, String value) {
       this.name = name;
       this.value = value;
  }
}

Cell

各マス目の状態を表すクラス このセルはプロパティとして、Playerクラスのインスンタスを持ちます。 model/Cell.javaを作成します。

public class Cell {

   public Player player;

   public Cell(Player player) {
       this.player = player;
  }

   public boolean isEmpty() {
       return player == null || StringUtility.isNullOrEmpty(player.value);
  }
}

Game

実際のゲームを表すクラス 二人のPlayerクラス、9つのCellクラスを持つ。

switchPlayer()メソッドでプレイヤーの順番を入れ替える ゲーム終了時、currentPlayerになっているプレイヤーが勝ちとなる

model/Game.javaを作成します

public class Game {

   private static final String TAG = Game.class.getSimpleName();
   private static final int BOARD_SIZE = 3;

   public Player player1;
   public Player player2;

   public Player currentPlayer = player1;
   public Cell[][] cells;

   public MutableLiveData<Player> winner = new MutableLiveData<>();

   public Game(String playerOne, String playerTwo) {
       cells = new Cell[BOARD_SIZE][BOARD_SIZE];
       player1 = new Player(playerOne, "x");
       player2 = new Player(playerTwo, "o");
       currentPlayer = player1;
  }

   public void switchPlayer() {
       currentPlayer = currentPlayer == player1 ? player2 : player1;
  }
}

ゲーム終了の判定

以下のときにゲームが終了したと判断する

  • ボード上の水平、垂直、斜めに3つ同じマークが並んだとき
  • 全てのマスが埋まったとき

それに伴って各メソッドを定義していきます。

model/Game.javaに追記します

// Gameが終了かどうかを判定
public boolean hasGameEnded() {
   // どれか3つ揃ったら、currentPlayerが勝ち
   if (hasThreeSameHorizontalCells() || hasThreeSameVerticalCells() || hasThreeSameDiagonalCells()) {
       winner.setValue(currentPlayer);
       return true;
  }

   // マスが全て埋まったら引き分け
   if (isBoardFull()) {
       winner.setValue(null);
       return true;
  }

   return false;
}

// 垂直に揃っているかを確認するメソッド
public boolean hasThreeSameHorizontalCells() {
   try {
       for (int i = 0; i < BOARD_SIZE; i++)
           if (areEqual(cells[i][0], cells[i][1], cells[i][2]))
               return true;

       return false;
  } catch (NullPointerException e) {
       Log.e(TAG, e.getMessage());
       return false;
  }
}

// 水平に揃っているかを確認するメソッド
public boolean hasThreeSameVerticalCells() {
   try {
       for (int i = 0; i < BOARD_SIZE; i++)
           if (areEqual(cells[0][i], cells[1][i], cells[2][i]))
               return true;

       return false;
  } catch (NullPointerException e) {
       Log.e(TAG, e.getMessage());
       return false;
  }
}

// 斜めに揃っているかを悪忍するメソッド
public boolean hasThreeSameDiagonalCells() {
   try {
       return areEqual(cells[0][0], cells[1][1], cells[2][2]) || areEqual(cells[0][2], cells[1][1], cells[2][0]);
  } catch (NullPointerException e) {
       Log.e(TAG, e.getMessage());
       return false;
  }
}

// セルがnull または空かを確認
public boolean isBoardFull() {
   for (Cell[] row : cells)
       for (Cell cell : row)
           if (cell == null || cell.isEmpty())
               return false;
   return true;
}

// 引数で与えられたセルのplayer.valueが全て等しいかを確認
private boolean areEqual(Cell... cells) {
   if (cells == null || cells.length == 0)
       return false;

   for (Cell cell : cells)
       if (cell == null || cell.player.value == null || cell.player.value.length() == 0)
           return false;

   Cell comparisonBase = cells[0];
   for (int i = 1; i < cells.length; i++)
       if (!comparisonBase.player.value.equals(cells[i].player.value))
           return false;

   return true;
}

ゲーム終了時の初期化メソッド

// ゲーム終了時各値を初期化
public void reset() {
   player1 = null;
   player2 = null;
   currentPlayer = null;
   cells = null;
}

Model View

ViewModelではViewのインスタンスを持たない。 つまりViewを知らない。

では、ViewModelとViewはどのように通信するのか。 これには以下の方法がある。

  • eventBus
  • RxAndroid
  • Observerパターン
  • LiveData

ここではLiveDataを使用します。

LibeDataとは

動的なデータをラップし、それを観測できるというもの

AがLiveDataインスタンスで、Bがそれを観測している場合、 Aのデータが変更されると、Bにそれが通知され、変更後の値が送信される。

これをMVVMにあてはめると、

MVVMでLiveDataインスタンスを持っていて、それをViewから観測していることになる。 つまり、MVVMがViewを知らなくてよい。

具体的には、MVVMではゲーム終了時にViewにそれを通知する。 これによりViewはUIを更新することができる。

データバインディングの有効化

build.gradleに以下を追記します。

dataBinding {
  enabled = true
}

共通処理の追加

utilities/StringUtility.java

public static String stringFromNumbers(int... numbers) {
   StringBuilder sNumbers = new StringBuilder();
   for (int number : numbers)
       sNumbers.append(number);
   return sNumbers.toString();
}

GameViewModel

viewmodel/GameViewModel.javaを作成

public class GameViewModel extends ViewModel {

   public ObservableArrayMap<String, String> cells;
   private Game game;

   public void init(String player1, String player2) {
       game = new Game(player1, player2);
       cells = new ObservableArrayMap<>();
  }

   public void onClickedCellAt(int row, int column) {
       if (game.cells[row][column] == null) {
           game.cells[row][column] = new Cell(game.currentPlayer);
           cells.put(stringFromNumbers(row, column), game.currentPlayer.value);
           if (game.hasGameEnded())
               game.reset();
           else
               game.switchPlayer();
      }
  }

   public LiveData<Player> getWinner() {
       return game.winner;
  }
}
  • onClickedCellAt()は、xmlファイル内でバインドされていて、セルをクリックするたびに実行される* getWinner()は、Viewから観測されているメソッド、ゲーム終了と勝者を通知する。

コメント