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

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

マイケル
今回はオセロAI開発の続きを行なっていきます!
しばらく脱線していたので大分久しぶりになりますね。
しばらく脱線していたので大分久しぶりになりますね。
↑前回までの記事

エレキベア
今度はどんなAIを作っていくクマ??

マイケル
今回は新たなアルゴリズムの実装ではなくて、これまで作ったAIを組み合わせて「キャラクターらしさ」を表現するのに挑戦してみようと思うよ!
とはいえ特に難しいことはやらないので、制作日記的な感じで見ていただければと思います!
とはいえ特に難しいことはやらないので、制作日記的な感じで見ていただければと思います!

エレキベア
なるほどクマ
当初の目的にしていたキャラクターのAIを作ってみるクマね
当初の目的にしていたキャラクターのAIを作ってみるクマね

マイケル
完成イメージはこんな感じ!
盤面の状況に合わせてキャラクターの動きや打つ手が変化するようになっています。
盤面の状況に合わせてキャラクターの動きや打つ手が変化するようになっています。



エレキベア
キャラクターが本当に考えて打ってるみたいで面白いクマ〜〜
簡単な感情を付けてる感じなのクマね
簡単な感情を付けてる感じなのクマね

マイケル
これでキャラクター自身とオセロしているように感じてくれたら嬉しいな!
今回の分もソースコードだけ上げています!こちらも是非ご参照ください!
今回の分もソースコードだけ上げています!こちらも是非ご参照ください!
unity-reversi-game-scripts – GitHub (v0.4.0)
↑今回の実装内容

マイケル
それではさっそく実装した内容を見ていこう!
・キャラクターAIの実装内容
・Prefabやアニメーションの設定内容
の順番で振り返っていきます!
・キャラクターAIの実装内容
・Prefabやアニメーションの設定内容
の順番で振り返っていきます!
キャラクターAIの実装

マイケル
まずはキャラクターのAI実装について!
感情パラメータの設定

マイケル
こちらは PlayerEmotionという感情パラメータを用意して、
「通常」「焦り」「悲しみ」という3種類の感情に分けることにしました。
「通常」「焦り」「悲しみ」という3種類の感情に分けることにしました。
/// <summary>
/// 感情値
/// </summary>
protected enum PlayerEmotion
{
Normal = 0, // 通常
Heat = 1, // 焦り
Sad = -1, // 悲しみ
}
protected PlayerEmotion Emotion;
・・・略・・・
/// <summary>
/// 感情値を設定
/// </summary>
/// <param name="myStoneStateRate">自分のストーン比率</param>
public void SetEmotionParameter(float myStoneStateRate)
{
if (_playerAnimationBehaviour == null) return;
Emotion = PlayerEmotion.Normal;
if (myStoneStateRate < 0.6f) Emotion = PlayerEmotion.Heat; // やばい時:焦り
if (myStoneStateRate < 0.3f) Emotion = PlayerEmotion.Sad; // 更にやばい時:悲しみ
_playerAnimationBehaviour.SetEmotionInt((int) Emotion);
}
↑感情パラメータ
マイケル
今回は簡単に、自分の石の比率を見て振り分けるようにしています。
(時間があればファジィ論理を使った曖昧な感情表現にも挑戦してみたいです・・・)
(時間があればファジィ論理を使った曖昧な感情表現にも挑戦してみたいです・・・)

マイケル
感情パラメータの設定は、石を置いた直後に設定し直すようにしました。
/// <summary>
/// ストーンを置く
/// </summary>
/// <param name="stoneState"></param>
/// <param name="x"></param>
/// <param name="z"></param>
private void PutStone(StoneState stoneState, int x, int z)
{
// ストーン置く
if (_stoneManager.PutStone(stoneState, x, z))
{
// ストーン比率からエモーションを設定
_player1.SetEmotionParameter(_stoneManager.GetStoneStateRate(_player1.MyStoneState));
_player2.SetEmotionParameter(_stoneManager.GetStoneStateRate(_player2.MyStoneState));
・・・略・・・
↑石を置いた後に感情パラメータを再設定
エレキベア
簡易的とはいえど感情を持つのはアツいクマね
各キャラクターの挙動と実装

マイケル
そして次は、設定した感情に対してどのような挙動にするかについてです!
現状、作成したアルゴリズムは下記の6種類なので、これらを組み合わせてキャラクターらしい選択をするように実装してみます。
現状、作成したアルゴリズムは下記の6種類なので、これらを組み合わせてキャラクターらしい選択をするように実装してみます。
- ランダム
- MiniMax法
- モンテカルロ法
- 強化学習(弱)
- 強化学習(強)
- 強化学習(MiniMonteキラー)

エレキベア
強化学習で出来た中途半端なAIも活用出来そうクマね

マイケル
これらを使って、下記のような表にしてみました。
作成するAIキャラクターは5種類で、それぞれの個性に合わせて打つ手を変えています。
(調整次第で変更の可能性は有り)
作成するAIキャラクターは5種類で、それぞれの個性に合わせて打つ手を変えています。
(調整次第で変更の可能性は有り)
名前 | 通常 | 焦り | 悲しい | 補足 |
---|---|---|---|---|
ピカル | 強化学習(弱) | ランダム | 強化学習(弱) | 簡単に勝てるようにする。 |
マイケル | 強化学習(強) | ランダム | モンテカルロ | 焦った時は慌ててランダム、負けそうな時は考える。 |
エレキベア | MiniMax | MiniMax | ランダム | 基本はMiniMaxだが、負けそうな時は投げやり。 |
ゴロヤン | MiniMax→モンテカルロ たまにランダムで打つ | 強化学習 (MiniMonteキラー) | モンテカルロ | 行動が読めないようにする。 |
ラスボス | MiniMax → モンテカルロ | モンテカルロ | モンテカルロ | 現状で一番強いMiniMaxとモンテカルロの組み合わせ。 |
↑各キャラクターごとのアルゴリズム選択

エレキベア
ランダムも活用することで個性が出るのは面白いクマね

マイケル
こうして見るとAIの種類もかなり増えてきたなぁとしみじみ思います。
namespace Reversi.Players
{
/// <summary>
/// プレイヤーの種類
/// 追加したらPlayerFactoryも修正する
/// </summary>
public enum PlayerType
{
None,
InputPlayer, // 入力プレイヤー
RandomAIPlayer, // ランダムに置くAI
MiniMaxAIPlayer, // MiniMaxアルゴリズムで置くAI
MonteCarloAIPlayer, // モンテカルロ法で置くAI
MiniMaxMonteAIPlayer, // 序盤MiniMax法、終盤モンテカルロ法のAI
MlAgentAIPlayer, // MLAgentsを使用したAI
MiniMonteKillerAIPlayer, // MiniMonteAI対策に特化したAI
MlAgentAIPlayerLearn1, // MLAgentsを使用したAI(学習用1)
MlAgentAIPlayerLearn2, // MLAgentsを使用したAI(学習用2)
// ----- 以下が本番で対戦できるAI -----
PikaruPlayer, // ピカル
MichaelPlayer, // マイケル
ElekiBearPlayer, // エレキベア
GoloyanPlayer, // ゴロヤン
ZeroPlayer, // ゼロ
MiniMaxAIRobotPlayer, // AIロボ(MiniMax)
MonteCarloAIRobotPlayer, // AIロボ(モンテカルロ)
MlAgentAIRobotPlayer, // AIロボ(強化学習)
}
}
↑AIの種類がかなり増えた
マイケル
アルゴリズムを選択する例として、エレキベアは下記のように実装しています。
打つ手を選ぶ際に感情パラメータを見てアルゴリズムを選択しているだけですね。
打つ手を選ぶ際に感情パラメータを見てアルゴリズムを選択しているだけですね。
/// <summary>
/// 選択するストーンを考える
/// </summary>
private async void StartThinkAsync()
{
// 考える時間
await WaitSelectTime(200);
// 早すぎると上手くいかないので1フレームは待つ
await UniTask.DelayFrame(1);
// 感情によって選択手法を変える
switch (Emotion)
{
// 通常、焦り:MiniMax
case PlayerEmotion.Normal:
case PlayerEmotion.Heat:
SelectStoneIndex = await SearchMiniMaxStoneTask();
break;
// 悲しい:ランダム
case PlayerEmotion.Sad:
SelectStoneIndex = AIAlgorithm.GetRandomStoneIndex(StoneStates, MyStoneState);
break;
}
}
/// <summary>
/// MiniMax法でのストーン探索処理
/// </summary>
private async UniTask<StoneIndex> SearchMiniMaxStoneTask()
{
await UniTask.SwitchToThreadPool(); // 時間がかかるため別スレッドで実行
var result = AIAlgorithm.SearchNegaAlphaStone(StoneStates, MyStoneState, 3, true);
await UniTask.SwitchToMainThread();
return result;
}

エレキベア
もう少し強くしてほしかったクマ〜〜〜

マイケル
AIの実装内容は以上になります!
アニメーション作成と描画

マイケル
中身ができたところで、見た目上でも楽しめるように
各キャラクターのモデルやアニメーションを作成していきます。
各キャラクターのモデルやアニメーションを作成していきます。
Prefabの作成

マイケル
プレイヤーやシミュレーションだけのAIモデルを合わせると、
作成したモデルは下記の7種類になります。
Prefabを作成する際、Prefab Variantとして作成しておくと、後からfbxファイルのみ差し替えることが可能になるためおすすめです!
作成したモデルは下記の7種類になります。
Prefabを作成する際、Prefab Variantとして作成しておくと、後からfbxファイルのみ差し替えることが可能になるためおすすめです!


エレキベア
これだけ作るとなるとかなり大変そうクマ〜〜
(オセロでこんなに作る必要があったのクマか・・・)
(オセロでこんなに作る必要があったのクマか・・・)

マイケル
Blenderを使用して作成したけど、使い方を思い出しながらで大変だったよ・・・
キャラモデル作成方法についても記事にしてありますので、よければこちらもご覧ください!
キャラモデル作成方法についても記事にしてありますので、よければこちらもご覧ください!
↑Blenderでキャラモデルを作成したメモ

マイケル
各モデルにはBlendShapesも設定してあり、
表情も変更できるようにしてあります!
表情も変更できるようにしてあります!



エレキベア
これは嬉しいクマ〜〜〜

マイケル
なお、シェーダについてはURP版のユニティちゃんトゥーンシェーダを使用させていただきました!
簡単にいい感じのトゥーンシェーダが作れるのでありがたいですね!
簡単にいい感じのトゥーンシェーダが作れるのでありがたいですね!

GitHub – UnityChanToonShaderVer2_Project – urp-2.5.1

エレキベア
URP版も出していただけていたクマね
アニメーションの作成

マイケル
アニメーションは下記のように設定しています。
ぐちゃぐちゃで大変見苦しいですが、感情による動きの変化と石を置くアニメーション、結果のアニメーションをそれぞれ作っています。
ぐちゃぐちゃで大変見苦しいですが、感情による動きの変化と石を置くアニメーション、結果のアニメーションをそれぞれ作っています。



マイケル
これをキャラクターの数だけ作成しました・・・。

エレキベア
これを7種類作るクマか・・・
えっぐ・・・
えっぐ・・・

マイケル
各キャラクターのアニメーションはAnimator Override Controllerとして作成しておけば、初めに作ったAnimator Controllerをベースに作成することができるためおすすめです。
なお、アニメーションはVery Animationを使用して作成しました。
なお、アニメーションはVery Animationを使用して作成しました。


エレキベア
オーバーライドして作成できたのクマね〜〜

マイケル
アニメーションのパラメータは下記のように受け口を作って設定するようにしました。
これでアニメーション設定は完了です!!
これでアニメーション設定は完了です!!
using System;
using Reversi.Const;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Reversi.Players.Animation
{
/// <summary>
/// プレイヤーアニメーションBehaviour
/// </summary>
public class PlayerAnimationBehaviour : MonoBehaviour
{
/// <summary>
/// アニメーション関連
/// </summary>
private Animator _animator;
private static readonly string AnimParamEmotion = "EmotionInt";
private static readonly string AnimParamResult = "ResultInt";
private static readonly string AnimParamPutTrigger = "PutTrigger";
private static readonly string AnimParamWaitBool = "WaitBool";
private void Awake()
{
// Animatorを取得
_animator = gameObject.GetComponent<Animator>();
StartAnimation();
}
private void OnEnable()
{
StartAnimation();
}
private void StartAnimation()
{
// タイトル画面の場合、Waitから開始する
if (SceneManager.GetActiveScene().name == GameConst.SceneNameTitle)
{
_animator.SetBool(AnimParamWaitBool, true);
}
}
/// <summary>
/// Putアニメーション開始
/// </summary>
public void StartPutAnimation()
{
_animator.SetTrigger(AnimParamPutTrigger);
}
/// <summary>
/// 感情値を設定
/// </summary>
/// <param name="emotion"></param>
public void SetEmotionInt(int emotion)
{
_animator.SetInteger(AnimParamEmotion, emotion);
}
/// <summary>
/// 結果値を設定
/// </summary>
/// <param name="result"></param>
public void SetResult(int result)
{
_animator.SetInteger(AnimParamResult, result);
}
}
}
↑アニメーションパラメータの設定
エレキベア
あとは表示するだけクマ〜〜〜
キャラクターの描画

マイケル
キャラクターは下記のようにUI上に表示させています。
モデル自体は3D空間上に並べて、それぞれのカメラからRender Textureに描画するよう設定しました。
モデル自体は3D空間上に並べて、それぞれのカメラからRender Textureに描画するよう設定しました。



マイケル
Render Textureとカメラ、UIの設定は下記のようにしています。
カメラのCulling Maskにレイヤーを設定することで特定のレイヤーのみ表示させることができます。
カメラのCulling Maskにレイヤーを設定することで特定のレイヤーのみ表示させることができます。




マイケル
作成したプレイヤーへのレイヤー設定と、カメラワークの調整は下記のようにしました。
アタッチしたオブジェクトの全ての子オブジェクトに対してレイヤーを設定するようにしています。
アタッチしたオブジェクトの全ての子オブジェクトに対してレイヤーを設定するようにしています。
using Reversi.Const;
using Reversi.Extensions;
using UnityEngine;
namespace Reversi.Players.Display
{
/// <summary>
/// プレイヤー表示用Behaviour
/// </summary>
public class PlayerDisplayBehaviour : MonoBehaviour
{
/// <summary>
/// プレイヤーカメラ
/// </summary>
[SerializeField] private GameObject playerCamera;
[SerializeField] private PlayerCameraOffsetInfos playerCameraOffsetInfos;
public void Initialize(PlayerType playerType)
{
// 自身に設定されているレイヤーを子オブジェクト以下にも設定してRenderTextureに描画する
var layer = gameObject.layer;
gameObject.SetLayerRecursively(layer);
// カメラ位置を調整
var isReverseBoard = LayerMask.LayerToName(layer) == GameConst.LayerNamePlayer2;
var cameraOffsetInfo = playerCameraOffsetInfos.GetCameraOffsetInfo(playerType, isReverseBoard);
playerCamera.transform.localPosition = cameraOffsetInfo.cameraOffset.transform.localPosition;
playerCamera.transform.localRotation = cameraOffsetInfo.cameraOffset.transform.localRotation;
}
}
}
↑レイヤーの設定とカメラ位置の調整using UnityEngine;
namespace Reversi.Extensions
{
public static class GameObjectExtensions
{
/// <summary>
/// 子オブジェクト含めてLayerを設定する
/// </summary>
public static void SetLayerRecursively(this GameObject self, int layer)
{
self.layer = layer;
foreach (Transform t in self.transform)
{
SetLayerRecursively(t.gameObject, layer);
}
}
}
}
↑子オブジェクト全てにレイヤーを設定する拡張実装
エレキベア
これでプレイヤー描画もバッチリクマね
キャラクターの選択処理

マイケル
最後におまけとして、キャラクター選択画面のカメラ切り替えについても紹介します!
下記のように選択したキャラクターの前にカメラが移動するよう実装しました。
下記のように選択したキャラクターの前にカメラが移動するよう実装しました。



マイケル
こちらはCinemachineを使用しましたが、地道にカメラを置いて、
キャラクター選択時にそれぞれのカメラに切り替えることで実現しています。
キャラクター選択時にそれぞれのカメラに切り替えることで実現しています。



マイケル
切替処理は下記の通りです!
種類に応じて切り替えるだけのシンプルなクラスになっています。
種類に応じて切り替えるだけのシンプルなクラスになっています。
using System;
using System.Collections.Generic;
using Cinemachine;
using Reversi.Players;
using UnityEngine;
namespace Reversi.Cameras
{
/// <summary>
/// プレイヤー選択カメラクラス
/// </summary>
public class SelectPlayerCamerasBehaviour : MonoBehaviour
{
/// <summary>
/// カメラ情報
/// </summary>
[SerializeField] private List<CameraInfo> cameraInfos;
[Serializable] private class CameraInfo
{
/// <summary>
/// プレイヤー種類
/// </summary>
public PlayerType playerType;
/// <summary>
/// プレイヤー種類に応じたカメラ
/// </summary>
public CinemachineVirtualCamera virtualCamera;
}
/// <summary>
/// プレイヤー種類に応じたカメラを表示する
/// </summary>
/// <param name="playerType"></param>
public void ShowCamera(PlayerType playerType)
{
// プレイヤー種類に応じたカメラを取得して表示
var cameraInfo = cameraInfos.Find(info => info.playerType == playerType);
if (cameraInfo == null)
{
Debug.LogError("noting set camera!! type: " + playerType);
return;
}
cameraInfo.virtualCamera.Priority = 1;
}
public void SetAllPriority(int priority)
{
cameraInfos.ForEach(info => info.virtualCamera.Priority = priority);
}
}
}
↑キャラクター切替処理
エレキベア
カメラワークが入るだけで選択画面も楽しいクマね〜〜〜

マイケル
キャラクター関連の紹介はこれにて完了です!!
おわりに

マイケル
というわけで今回はキャラクターらしさを表現したAIの作成でした!
どうだったかな??
どうだったかな??

エレキベア
これまで堅実なAIの作成も楽しかったクマが、
やっぱりキャラクターが入ると楽しさ倍増クマ〜〜〜〜
生きてるように動くのは見てて楽しいクマね
やっぱりキャラクターが入ると楽しさ倍増クマ〜〜〜〜
生きてるように動くのは見てて楽しいクマね

マイケル
命が芽生えたみたいで楽しいよね!!
今回は簡単な感情しか入れなかったけど、勉強してもっと複雑な表現にも挑戦してみたいな!
今回は簡単な感情しか入れなかったけど、勉強してもっと複雑な表現にも挑戦してみたいな!

エレキベア
エレキベアを具現化してくれクマ

マイケル
とりあえずオセロ開発はキャラクター制作に時間がかかっちゃったけど、
あとはUI周りや音源周りを作成すれば完成だ!
完成したらまた改めて記事にしようと思います!
あとはUI周りや音源周りを作成すれば完成だ!
完成したらまた改めて記事にしようと思います!

マイケル
それでは今日はこの辺で!
アデューー!!
アデューー!!

エレキベア
クマ〜〜〜〜
【Unity】第五回 オセロAI開発 〜キャラクターらしさを表現したAIの作成〜【ゲームAI】〜完〜
コメント