
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜

マイケル
突然だけど題名にもある通り、
今日からはしばらくオセロAIの開発を進めていくよ!
今日からはしばらくオセロAIの開発を進めていくよ!

エレキベア
オセロクマか〜〜〜
単純だからAI作成入門によさそうクマね
単純だからAI作成入門によさそうクマね

マイケル
まさしくAI作成の方に集中できそうだよね!
とりあえず今回は準備としてオセロゲームの基盤を作ったから
実装した内容について簡単に紹介していくよ!
とりあえず今回は準備としてオセロゲームの基盤を作ったから
実装した内容について簡単に紹介していくよ!


エレキベア
これは中々ちゃんとしてるクマね

マイケル
それと今回はランダムに石を置くAIまでしか作成していないけど、
次回以降、下記のような有名なアルゴリズムもいくつか実装する予定だよ!
次回以降、下記のような有名なアルゴリズムもいくつか実装する予定だよ!
- MiniMax法
- モンテカルロ法
- 何らかの機械学習手法

エレキベア
なんか聞いたことはあるクマね
楽しみクマ〜〜〜
楽しみクマ〜〜〜

マイケル
そして強いAIを作るだけじゃつまらないので、上記アルゴリズムをミックスしながら
キャラクターと実際に対戦しているような仕組みも作ってみようと思っています。
作ったゲームは公開するまでを目標に頑張ってみます!
キャラクターと実際に対戦しているような仕組みも作ってみようと思っています。
作ったゲームは公開するまでを目標に頑張ってみます!

エレキベア
エレキベアも是非参戦させてほしいクマ〜〜〜
ゲーム全体構成

マイケル
それでは全体の構成を見ていきます!
GitHubにも上げていますが、アセットをいくつも使用しているため
今回はソースコードのみ公開としています。
参考程度にお使いください!
GitHubにも上げていますが、アセットをいくつも使用しているため
今回はソースコードのみ公開としています。
参考程度にお使いください!
GitHub – masarito617/unity-reversi-game-scripts

エレキベア
ライセンス周りが面倒くさいクマからね
検証環境

マイケル
検証環境は下記の通り!
[Unityバージョン]
2021.3.1f1 LTS
[使用パッケージ]
・UniRx
・UniTask
・VContainer
・DOTween
・Japanese School Classroom

エレキベア
UniRx、VContainerと流行りのアセットが盛り沢山クマね

マイケル
簡単なゲームだし練習がてら使ってみようと思ってね!
それとJapanese School Classroomは有料のアセットだけど、クオリティの高い教室が手に入って汎用性も高いからおすすめです!!
それとJapanese School Classroomは有料のアセットだけど、クオリティの高い教室が手に入って汎用性も高いからおすすめです!!

エレキベア
何か前も使ってたクマね

マイケル
たくさん使わせていただいてます!
スクリプト構成

マイケル
各クラス、パッケージの関係性としてはざっくりと下記のようになっています!
VContainerやUniRxを使用した構成で、DIコンテナからクラスを注入したり、UI側からManagersの状態を監視して更新するようにしています。
VContainerやUniRxを使用した構成で、DIコンテナからクラスを注入したり、UI側からManagersの状態を監視して更新するようにしています。

パッケージ | 役割 |
LifeTimeScopes | クラスの注入やエントリーポイントの指定 |
EntryPoints | 全体の処理のエントリーポイント |
Managers | ゲーム全体やプレイヤー、石といったオブジェクトの管理 |
UIs | UIの表示 |
Players | プレイヤーとAIに関連する処理 |
Stones | 石と関連する処理 |

マイケル
まだまだ甘いところはあるかと思いますが、
下記書籍のUniRx、Zenjectの章や、とりすーぷさんのコードを参考にさせていただきました!いつも勉強させていただいてます!
下記書籍のUniRx、Zenjectの章や、とりすーぷさんのコードを参考にさせていただきました!いつも勉強させていただいてます!
Unityゲーム プログラミング・バイブル 2nd Generation
参考記事:
グローバルゲームジャムでクラス設計をやった話2020

エレキベア
UniRx、UniTaskの書籍を出した方クマね

マイケル
LifeTimeScope、EntryPointのコードは下記のようになっています。
この辺りの詳細について知りたい方は、VContainerの公式チュートリアルをご参照ください!
この辺りの詳細について知りたい方は、VContainerの公式チュートリアルをご参照ください!
公式チュートリアル:
Hello World | VContainer
using Reversi.EntryPoints;
using Reversi.Managers;
using Reversi.Players.Input;
using Reversi.Players.Input.Impl;
using Reversi.UIs;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Reversi.LifeTimeScopes
{
/// <summary>
/// Gameシーン LifeTimeScope
/// </summary>
public class GameLifeTimeScope : LifetimeScope
{
[SerializeField] private GamePresenter gamePresenter;
protected override void Configure(IContainerBuilder builder)
{
// 各クラスを登録
builder.Register<GameManager>(Lifetime.Singleton);
builder.Register<BoardManager>(Lifetime.Singleton);
builder.Register<StoneManager>(Lifetime.Singleton);
builder.Register<PlayerManager>(Lifetime.Singleton);
builder.Register<IInputEventProvider, InputEventProvider>(Lifetime.Singleton);
// コンポーネント登録
builder.RegisterComponent(gamePresenter);
// エントリーポイント登録
builder.RegisterEntryPoint<GameEntryPoint>();
}
}
}
↑依存関係、エントリーポイントの定義using Reversi.Managers;
using VContainer;
using VContainer.Unity;
namespace Reversi.EntryPoints
{
/// <summary>
/// Gameシーン EntryPoint
/// </summary>
public class GameEntryPoint : IStartable, ITickable
{
private readonly GameManager _gameManager;
[Inject]
public GameEntryPoint(GameManager gameManager)
{
_gameManager = gameManager;
}
void IStartable.Start()
{
_gameManager.OnStart();
}
void ITickable.Tick()
{
_gameManager.OnUpdate();
}
}
}
↑用意されたインターフェースを使用することでUnityEngineを使わずにライフサイクルを定義できる
エレキベア
これがいわゆるDIコンテナというやつクマね

マイケル
そしてUI(Presenter)からの監視は下記のようになっています。
Managerクラス側にReactivePropertyを定義してその状態を監視するシンプルな構成です。
Managerクラス側にReactivePropertyを定義してその状態を監視するシンプルな構成です。
/// <summary>
/// ストーン管理クラス
/// </summary>
public class StoneManager
{
/// <summary<
/// 各色ごとのストーンの数
/// </summary<
private readonly ReactiveProperty<int> _whiteStoneCount = new(0);
private readonly ReactiveProperty<int> _blackStoneCount = new(0);
public IReadOnlyReactiveProperty<int> WhiteStoneCount => _whiteStoneCount;
public IReadOnlyReactiveProperty<int> BlackStoneCount => _blackStoneCount;
・・・略・・・
}
↑ReactivePropertyの定義using Reversi.Managers;
using TMPro;
using UniRx;
using UnityEngine;
using VContainer;
namespace Reversi.UIs
{
/// <summary>
/// GamePresenter
/// </summary>
public class GamePresenter : MonoBehaviour
{
[Inject] private StoneManager _stoneManager;
[Inject] private GameManager _gameManager;
[SerializeField] private TextMeshProUGUI blackCountText;
[SerializeField] private TextMeshProUGUI whiteCountText;
[SerializeField] private TextMeshProUGUI resultText;
private void Start()
{
blackCountText.text = "0";
whiteCountText.text = "0";
resultText.text = "";
// ストーン数表示
_stoneManager
.BlackStoneCount
.Subscribe(x => blackCountText.text = x.ToString())
.AddTo(this);
_stoneManager
.WhiteStoneCount
.Subscribe(x => whiteCountText.text = x.ToString())
.AddTo(this);
・・・略・・・
}
}
}
↑Presenter側で状態を監視してUIを更新する
エレキベア
Manager側からUIを全く気にせずに済むのは気持ちいいクマね
オブジェクト構成

マイケル
次はオブジェクトの構成についてです。
シーン内は下記のようになっていて、LifeTimeScope、Presenter、Canvasのオブジェクトを用意してそれぞれアタッチしています。
また、ボードに関しては土台のみ先に置いています。
シーン内は下記のようになっていて、LifeTimeScope、Presenter、Canvasのオブジェクトを用意してそれぞれアタッチしています。
また、ボードに関しては土台のみ先に置いています。


マイケル
ボードの上のセル(マス)については、下記のようにPrefabを用意してスクリプト内で生成しています。

/// <summary>
/// ボード生成処理
/// </summary>
public void GenerateBoard()
{
_cellPositions = new Vector3[CellSideCount, CellSideCount];
for (var x = 0; x < CellSideCount; x++)
{
for (var z = 0; z < CellSideCount; z++)
{
// セル生成
var cell = Object.Instantiate(_boardCellPrefab, _boardCellsBase.gameObject.transform);
cell.transform.localPosition = new Vector3(x, 0.4f, z);
// 位置を保持
_cellPositions[x, z] = cell.transform.localPosition;
}
}
}
/// <summary>
/// 指定セルの位置を取得
/// </summary>
/// <param name="x"></param>
/// <param name="z"></param>
/// <returns></returns>
public Vector3 GetCellPosition(int x, int z)
{
return _cellPositions[x, z];
}
↑セルの生成
マイケル
オセロの石についてもPrefabを用意してスクリプトから生成するようにしています。
置ける場合にフォーカスするエリアについても、Prefab内に含めています。
置ける場合にフォーカスするエリアについても、Prefab内に含めています。

/// <summary>
/// ストーン初期化
/// </summary>
/// <param name="cellSideCount">一辺あたりのセル数</param>
/// <param name="getCellPosition">ボードのセル位置取得処理</param>
public void InitializeStones(int cellSideCount, Func<int, int, Vector3> getCellPosition)
{
// 作成済のストーンを削除
foreach(Transform child in _stonesBase.transform){
UnityEngine.Object.Destroy(child.gameObject);
}
// ストーンをEmptyで初期化して設定
_viewStoneCores = new StoneBehaviour[cellSideCount, cellSideCount];
_stoneStates = new StoneState[cellSideCount, cellSideCount];
for (var x = 0; x < _viewStoneCores.GetLength(0); x++)
{
for (var z = 0; z < _viewStoneCores.GetLength(1); z++)
{
var stone = UnityEngine.Object.Instantiate(_stonePrefab, _stonesBase.transform, true);
stone.transform.localPosition = getCellPosition(x, z) + Vector3.up * StoneOffsetY;
_viewStoneCores[x, z] = stone.GetComponent<StoneBehaviour>();
_viewStoneCores[x, z].SetIndex(x, z);
_stoneStates[x, z] = StoneState.Empty;
}
}
// 中央のストーン状態を変更
var centerIndex1 = cellSideCount / 2;
var centerIndex2 = centerIndex1 - 1;
_stoneStates[centerIndex1, centerIndex1] = StoneState.White;
_stoneStates[centerIndex2, centerIndex1] = StoneState.Black;
_stoneStates[centerIndex1, centerIndex2] = StoneState.Black;
_stoneStates[centerIndex2, centerIndex2] = StoneState.White;
// ストーンの表示を更新
UpdateAllViewStones(_stoneStates);
}
↑石の生成処理
マイケル
そして、Managerクラスにはメンバ変数として
ボードに対する石の状態配列と、
表示用のオブジェクト配列を持たせています。
状態配列を変更した後に別途表示の更新を行う構成としました。
ボードに対する石の状態配列と、
表示用のオブジェクト配列を持たせています。
状態配列を変更した後に別途表示の更新を行う構成としました。
/// <summary>
/// ストーン配列
/// </summary>
private StoneState[,] _stoneStates;
private StoneBehaviour[,] _viewStoneCores; // 表示用
↑状態配列と表示用の配列 /// <summary>
/// ストーンの状態
/// </summary>
public enum StoneState
{
Empty = 0, // 何も無い
White = 1, // 白
Black = 2, // 黒
}
↑石の状態
エレキベア
最後に表示を更新することで見た目上の遅延を少なくしているクマね

マイケル
変更後の状態によって石の表示を変える処理は下記になります。
エフェクトやアニメーションもこの時生成するようにしています。
エフェクトやアニメーションもこの時生成するようにしています。
/// <summary>
/// ストーンの状態を変更する
/// </summary>
/// <param name="changeState">変更するストーン状態</param>
/// <param name="putStoneIndex">状態変更時に置かれたストーン位置(最初の設定時のみnull)</param>
public void ChangeViewState(StoneState changeState, StoneIndex putStoneIndex = null)
{
// 状態が変わらない場合は何も行わない
if (_viewState == changeState) return;
// 状態によって表示を切り替える
switch (changeState)
{
// Emptyの場合は非表示
case StoneState.Empty:
stoneObject.SetActive(false);
break;
// 色が指定されたら表示
case StoneState.White:
case StoneState.Black:
stoneObject.SetActive(true);
// 最終的に設定する位置、回転
var targetPosition = Vector3.zero;
var targetRotation = new Vector3(0.0f, 0.0f, changeState == StoneState.White ? 0.0f : 180.0f);
if (_viewState == StoneState.Empty)
{
// 初めて置かれる場合
stoneObject.transform.localEulerAngles = targetRotation;
if (putStoneIndex != null) StartPutEffect();
}
else
{
// 色が変わる場合
StartTurnAnimation(putStoneIndex, () =>
{
// 微妙なずれを防ぐためアニメーション完了時に直接指定する
stoneObject.transform.localPosition = targetPosition;
stoneObject.transform.localEulerAngles = targetRotation;
});
}
break;
}
// 状態の設定
_viewState = changeState;
}
↑石の表示を変える処理 /// <summary>
/// ひっくり返すアニメーション
/// </summary>
/// <param name="putStoneIndex"></param>
/// <param name="callback"></param>
private void StartTurnAnimation(StoneIndex putStoneIndex, Action callback)
{
// 色が変わる場合
var putVec = Index - putStoneIndex; // 置いた位置からのベクトル
var waitTime = putVec.GetLength() * 0.08f; // アニメーションを遅らせる時間(遠いほど開始を遅らせる)
var q = Quaternion.AngleAxis(180.0f, new Vector3(-putVec.Z, 0.0f, putVec.X)); // 90度回転させたクォータニオン(-Z,Xで指定)
// ひっくり返るアニメーションを実行
var sequence = DOTween.Sequence();
sequence.AppendInterval(waitTime);
sequence.Append(stoneObject.transform.DOLocalRotateQuaternion(q, 0.5f).SetRelative().SetEase(Ease.OutQuart));
sequence.Join(DOTween.Sequence()
.Append(stoneObject.transform.DOLocalMoveY(1, 0.25f).SetRelative().SetEase(Ease.OutQuart))
.Append(stoneObject.transform.DOLocalMoveY(-1, 0.2f).SetRelative().SetEase(Ease.InOutBounce)));
sequence.OnComplete(() =>
{
callback();
});
}
↑アニメーションはDOTWeenで作成
マイケル
準備するのはこれだけ!
以上の構成で下記のような画面が出来上がります。
以上の構成で下記のような画面が出来上がります。


エレキベア
これぞオセロクマ〜〜〜
メイン処理の解説

マイケル
ざっくりとした構成が分かったところで、
いよいよオセロのメイン処理を解説していきます。
いよいよオセロのメイン処理を解説していきます。

エレキベア
待ってましたクマ
ストーン関連の処理

マイケル
まずはオセロの石(ストーン)関連の処理を見ていきます。
ロジックについては主にStoneCalculator.csにまとめています。
ロジックについては主にStoneCalculator.csにまとめています。

マイケル
下記はオセロの肝となる、
「置いた位置からひっくり返せる石を判定する」処理になります。
「置いた位置からひっくり返せる石を判定する」処理になります。
/// <summary>
/// 置いた位置からひっくり返せるストーン情報を返却する
/// </summary>
/// <param name="stoneStates">チェックするストーン配列</param>
/// <param name="putState">置いた色</param>
/// <param name="putX">置いたX位置</param>
/// <param name="putZ">置いたZ位置</param>
/// <returns>ひっくり返せるストーン情報(Index)</returns>
public static List<StoneIndex> GetTurnStonesIndex(StoneState[,] stoneStates, StoneState putState, int putX, int putZ)
{
// 既にストーンが置かれていたら空で返却
var turnStonesIndex = new List<StoneIndex>();
if (stoneStates == null || stoneStates[putX, putZ] != StoneState.Empty) return turnStonesIndex;
// 8方向分のストーンを調べて返却する
foreach (var searchVec in SearchAllVectors)
{
turnStonesIndex.AddRange(GetTurnStonesIndex(stoneStates, putState, putX, putZ, searchVec));
}
return turnStonesIndex;
}
/// <summary>
/// 置いた位置から指定方向にひっくり返せるストーン情報を返却する
/// </summary>
/// <param name="stoneStates">チェックするストーン配列</param>
/// <param name="putState">置いた色</param>
/// <param name="putX">置いたX位置</param>
/// <param name="putZ">置いたZ位置</param>
/// <param name="searchVec">調べる方向(-1 or 0 or 1)</param>
/// <returns>ひっくり返せるストーン情報(Index)</returns>
private static List<StoneIndex> GetTurnStonesIndex(StoneState[,] stoneStates, StoneState putState, int putX, int putZ, SearchVector searchVec)
{
// 置く色と反対の色をターゲットにする
var targetState = putState == StoneState.Black ? StoneState.White : StoneState.Black;
// ひっくり返せるストーンを調べる
var turnStonesIndex = new List<StoneIndex>();
var x = putX;
var z = putZ;
while (true)
{
// 調べる方向に進む
x += searchVec.X;
z += searchVec.Z;
// indexが範囲外になったら調査終了
if (x < 0 || stoneStates.GetLength(0) <= x ||
z < 0 || stoneStates.GetLength(1) <= z)
{
break;
}
// 何も置いていなければ調査終了
var stoneState = stoneStates[x, z];
if (stoneState == StoneState.Empty)
{
break;
}
// 置いたストーンと同じ色の場合、調査したストーンを返却する
if (stoneState == putState)
{
return turnStonesIndex;
}
// ターゲットの色だったらリストに追加
if (stoneState == targetState)
{
turnStonesIndex.Add(new StoneIndex(x, z));
}
}
// 見つからなかったら空で返却
return new List<StoneIndex>();
}
/// <summary>
/// 探索方向
/// </summary>
private static readonly SearchVector[] SearchAllVectors = new SearchVector[]
{
new (1,0), // left
new (1,1), // left up diagonal
new (1,-1), // left down diagonal
new (-1,0), // right
new (-1,1), // right up diagonal
new (-1,-1), // right down diagonal
new (0,1), // up
new (0,-1), // down
};
private class SearchVector
{
public int X { get; private set; }
public int Z { get; private set; }
public SearchVector(int x, int z)
{
// 値をチェックする
var isValid = false;
isValid = isValid || (x != 0 && x != 1 && x != -1);
isValid = isValid || (z != 0 && z != 1 && z != -1);
isValid = isValid || (x == 0 && z == 0);
if (isValid) throw new ArgumentException("SearchVectorには0,1,-1のみ指定できます。((0,0)は除く)");
this.X = x;
this.Z = z;
}
}
↑ひっくり返せる石の判定
マイケル
石の配列と置く位置を受け取って8方向分調べています。
もっと効率いい方法もあるかと思いますが、そこはお許しください。。
もっと効率いい方法もあるかと思いますが、そこはお許しください。。

エレキベア
シンプルな判定クマね

マイケル
ちなみに返却しているStoneIndexについては、
下記のように位置情報を格納しているだけのクラスになります。
下記のように位置情報を格納しているだけのクラスになります。
using System;
using UnityEngine;
namespace Reversi.Stones.Stone
{
/// <summary>
/// ストーン配列に対応するインデックス
/// </summary>
public class StoneIndex : IEquatable<StoneIndex>
{
public int X { get; private set; }
public int Z { get; private set; }
public StoneIndex(int x, int z)
{
X = x;
Z = z;
}
public float GetLength()
{
return Mathf.Sqrt(X * X + Z * Z);
}
public static StoneIndex operator+ (StoneIndex a, StoneIndex b)
{
return new StoneIndex(a.X + b.X, a.Z + b.Z);
}
public static StoneIndex operator- (StoneIndex a, StoneIndex b)
{
return new StoneIndex(a.X - b.X, a.Z - b.Z);
}
public bool Equals(StoneIndex other)
{
if (other == null)
{
return false;
}
return X == other.X && Z == other.Z;
}
}
}
↑石の位置クラス
エレキベア
簡易Vectorクマね

マイケル
あとはこの関数を利用して
・置いた後の配列を返却する処理
・置くことが可能な石を返却する処理
も作成しています。
・置いた後の配列を返却する処理
・置くことが可能な石を返却する処理
も作成しています。
/// <summary>
/// 置いた後のストーン状態を返却する
/// </summary>
/// <param name="stoneStates"></param>
/// <param name="putState"></param>
/// <param name="putX"></param>
/// <param name="putZ"></param>
/// <param name="turnStonesIndex"></param>
/// <returns>置いた後のストーン状態</returns>
public static StoneState[,] GetPutStoneState(StoneState[,] stoneStates, StoneState putState, int putX, int putZ, List<StoneIndex> turnStonesIndex = null)
{
// 既に置いてある場合、そのまま返す
if (stoneStates == null || stoneStates[putX, putZ] != StoneState.Empty) return stoneStates;
// ひっくり返せるストーンが指定されていない場合、取得する
turnStonesIndex ??= GetTurnStonesIndex(stoneStates, putState, putX, putZ);
if (turnStonesIndex.Count == 0) return stoneStates;
// 引数からクローンを作成
var putStoneStates = stoneStates.Clone() as StoneState[,];
// ストーンを置く
if (putStoneStates != null)
{
putStoneStates[putX, putZ] = putState;
// ひっくり返す
foreach (var tuneStone in turnStonesIndex)
{
putStoneStates[tuneStone.X, tuneStone.Z] = putState;
}
}
return putStoneStates;
}
/// <summary>
/// 置くことが可能なストーン状態配列を返却する
/// </summary>
/// <param name="stoneStates"></param>
/// <param name="putState"></param>
/// <returns> 置くことが可能なストーン状態配列</returns>
public static List<StoneIndex> GetAllCanPutStonesIndex(StoneState[,] stoneStates, StoneState putState)
{
var canPutStones = new List<StoneIndex>();
for (var x = 0; x < stoneStates.GetLength(0); x++)
{
for (var z = 0; z < stoneStates.GetLength(1); z++)
{
// 置けるストーンなら追加
if (GetTurnStonesIndex(stoneStates, putState, x, z).Count > 0)
{
canPutStones.Add(new StoneIndex(x, z));
}
}
}
return canPutStones;
}
↑他のメイン処理
マイケル
これでほぼオセロは完成したようなもんですね!
テストも記述しているのでよければこちらもご参照ください!
テストも記述しているのでよければこちらもご参照ください!
GitHub – unity-reversi-game-scripts – StoneCalculatorTest.cs

エレキベア
ちょろいもんクマ〜〜
プレイヤー関連の処理

マイケル
次は実際にオセロをプレイするプレイヤー関連の処理についてです。
インターフェースとしてIPlayerを用意し、外部からは基本的にターン開始、ターン更新の処理しか呼べなくしています。
インターフェースとしてIPlayerを用意し、外部からは基本的にターン開始、ターン更新の処理しか呼べなくしています。
using Reversi.Stones.Stone;
namespace Reversi.Players
{
public interface IPlayer
{
/// <summary>
/// 自分のストーン状態(黒or白)
/// </summary>
public StoneState MyStoneState { get; }
public void StartTurn(StoneState[,] stoneStates);
public void UpdateTurn();
public bool IsInputPlayer();
}
}
↑プレイヤーのインターフェース
マイケル
そしてこれを継承したPlayerクラスで具体的な処理を実装しています。
後々作成する入力プレイヤーやAIに共通する処理を記述しています。
後々作成する入力プレイヤーやAIに共通する処理を記述しています。
using System;
using Reversi.Stones.Stone;
namespace Reversi.Players
{
/// <summary>
/// プレイヤー共通クラス
/// </summary>
public abstract class Player : IPlayer
{
/// <summary>
/// 自分のストーン状態(黒or白)
/// </summary>
protected readonly StoneState MyStoneState;
StoneState IPlayer.MyStoneState => MyStoneState;
/// <summary>
/// ストーン配列(AIチェック用)
/// </summary>
protected StoneState[,] StoneStates;
/// <summary>
/// 選択したストーン
/// 継承先のクラスでこの変数に設定する
/// </summary>
protected StoneIndex SelectStoneIndex;
/// <summary>
/// ストーン選択処理
/// </summary>
private readonly Action<StoneState, int, int> _putStoneAction;
protected Player(StoneState myStoneState, Action<StoneState, int, int> putStoneAction)
{
_putStoneAction = putStoneAction;
MyStoneState = myStoneState;
}
/// <summary>
/// ターン開始
/// </summary>
/// <param name="stoneStates"></param>
public void StartTurn(StoneState[,] stoneStates)
{
// ストーン配列を設定して初期化
StoneStates = stoneStates;
SelectStoneIndex = null;
// 思考開始
StartThink();
}
/// <summary>
/// ターン更新
/// </summary>
public void UpdateTurn()
{
// 思考更新
UpdateThink();
// ストーンが選択されたら置く
if (SelectStoneIndex == null) return;
_putStoneAction(MyStoneState, SelectStoneIndex.X, SelectStoneIndex.Z);
SelectStoneIndex = null; // 初期化
}
/// <summary>
/// 思考処理
/// </summary>
protected virtual void StartThink() { }
protected virtual void UpdateThink() { }
/// <summary>
/// 入力プレイヤーかどうか?
/// </summary>
public virtual bool IsInputPlayer()
{
// 基本的にfalse
return false;
}
}
}

マイケル
AI含む各プレイヤークラスではこのクラスを継承して、
StartThink()、UpdateThink()をオーバーライドすることで実装しています。
StartThink()、UpdateThink()をオーバーライドすることで実装しています。

エレキベア
Template Methodパターンクマね

マイケル
入力するプレイヤークラスは下記の通り!
UpdateTurn()をオーバーライドして入力検知した石を設定しています。
UpdateTurn()をオーバーライドして入力検知した石を設定しています。
using System;
using Reversi.Stones.Stone;
namespace Reversi.Players.Input
{
/// <summary>
/// 入力プレイヤー
/// </summary>
public class InputPlayer : Player
{
private readonly IInputEventProvider _inputEventProvider;
public InputPlayer(StoneState myStoneState, Action<StoneState, int, int> putStoneAction, IInputEventProvider inputEventProvider) : base(myStoneState, putStoneAction)
{
_inputEventProvider = inputEventProvider;
}
protected override void UpdateThink()
{
// 入力検知したストーンを取得
var selectStone = _inputEventProvider.GetSelectStone();
if (selectStone != null)
{
SelectStoneIndex = selectStone.Index;
}
}
public override bool IsInputPlayer()
{
return true;
}
}
}
↑入力検知した石を設定する
エレキベア
各プレイヤーのクラスはだいぶシンプルになるクマね
ランダムに置くAIの処理

マイケル
そして最後にランダムに石を置くAIを作成します!
置くことが可能な石を取得する処理を使ってランダムに返すようにしています。
置くことが可能な石を取得する処理を使ってランダムに返すようにしています。
using System;
using Cysharp.Threading.Tasks;
using Reversi.Stones;
using Reversi.Stones.Stone;
namespace Reversi.Players.AI
{
/// <summary>
/// ランダムにストーンを置くAI
/// </summary>
public class RandomAIPlayer : Player
{
public RandomAIPlayer(StoneState myStoneState, Action<StoneState, int, int> putStoneAction) : base(myStoneState, putStoneAction) { }
protected override void StartThink()
{
StartThinkAsync();
}
/// <summary>
/// 選択するストーンを考える
/// </summary>
private async void StartThinkAsync()
{
// 考える時間
await UniTask.Delay(500);
// ランダムに取得して設定
var canPutStones = StoneCalculator.GetAllCanPutStonesIndex(StoneStates, MyStoneState);
var randomIndex = UnityEngine.Random.Range(0, canPutStones.Count);
SelectStoneIndex = canPutStones[randomIndex];
}
}
}
↑ランダムに置くAI
エレキベア
これもだいぶシンプルにまとまったクマ〜〜

マイケル
以上でオセロゲームの基盤は完成です!!
おわりに

マイケル
というわけで今回はオセロゲーム基盤の作成でした!
どうだったかな??
どうだったかな??

エレキベア
設計もちゃんと考えて作るのは楽しかったクマ〜〜
今後ちゃんとしたAIを作るのが楽しみクマ
今後ちゃんとしたAIを作るのが楽しみクマ

マイケル
シンプルながら作って楽しかったね!
次回はオセロAI定番の、MiniMaxアルゴリズムを使ったAIの作成だ!
お楽しみに〜〜!
次回はオセロAI定番の、MiniMaxアルゴリズムを使ったAIの作成だ!
お楽しみに〜〜!

エレキベア
クマ〜〜〜〜
【Unity】第一回 オセロAI開発 〜ゲーム基盤とランダムに置くAIの作成〜【ゲームAI】〜完〜
次回の記事はこちら!
コメント