【Unity】第五回 オセロAI開発 〜キャラクターらしさを表現したAIの作成〜【ゲームAI】

Unity
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今回はオセロAI開発の続きを行なっていきます!
しばらく脱線していたので大分久しぶりになりますね。

↑前回までの記事

エレキベア
エレキベア
今度はどんなAIを作っていくクマ??
マイケル
マイケル
今回は新たなアルゴリズムの実装ではなくて、これまで作ったAIを組み合わせて「キャラクターらしさ」を表現するのに挑戦してみようと思うよ!
とはいえ特に難しいことはやらないので、制作日記的な感じで見ていただければと思います!
エレキベア
エレキベア
なるほどクマ
当初の目的にしていたキャラクターのAIを作ってみるクマね
マイケル
マイケル
完成イメージはこんな感じ!
盤面の状況に合わせてキャラクターの動きや打つ手が変化するようになっています。
01 battle
ScreenShot 2022 09 19 10 46 15↑盤面の状況に応じてキャラクターの挙動が変わる
エレキベア
エレキベア
キャラクターが本当に考えて打ってるみたいで面白いクマ〜〜
簡単な感情を付けてる感じなのクマね
マイケル
マイケル
これでキャラクター自身とオセロしているように感じてくれたら嬉しいな!
今回の分もソースコードだけ上げています!こちらも是非ご参照ください!

unity-reversi-game-scripts – GitHub (v0.4.0)

↑今回の実装内容

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

キャラクターAIの実装

マイケル
マイケル
まずはキャラクターのAI実装について!

感情パラメータの設定

マイケル
マイケル
こちらは PlayerEmotionという感情パラメータを用意して、
「通常」「焦り」「悲しみ」という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種類なので、これらを組み合わせてキャラクターらしい選択をするように実装してみます。
  • ランダム
  • MiniMax法
  • モンテカルロ法
  • 強化学習(弱)
  • 強化学習(強)
  • 強化学習(MiniMonteキラー)
エレキベア
エレキベア
強化学習で出来た中途半端なAIも活用出来そうクマね
マイケル
マイケル
これらを使って、下記のような表にしてみました。
作成するAIキャラクターは5種類で、それぞれの個性に合わせて打つ手を変えています。
(調整次第で変更の可能性は有り)
名前通常焦り悲しい補足
ピカル強化学習(弱)ランダム強化学習(弱)簡単に勝てるようにする。
マイケル強化学習(強)ランダムモンテカルロ焦った時は慌ててランダム、負けそうな時は考える。
エレキベアMiniMaxMiniMaxランダム基本は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ファイルのみ差し替えることが可能になるためおすすめです!
ScreenShot 2022 09 19 10 20 07↑作成したモデル
エレキベア
エレキベア
これだけ作るとなるとかなり大変そうクマ〜〜
(オセロでこんなに作る必要があったのクマか・・・)
マイケル
マイケル
Blenderを使用して作成したけど、使い方を思い出しながらで大変だったよ・・・
キャラモデル作成方法についても記事にしてありますので、よければこちらもご覧ください!

↑Blenderでキャラモデルを作成したメモ

マイケル
マイケル
各モデルにはBlendShapesも設定してあり、
表情も変更できるようにしてあります!
ScreenShot 2022 09 19 11 51 42
ScreenShot 2022 09 19 11 52 02↑BlendShapesも設定してある
エレキベア
エレキベア
これは嬉しいクマ〜〜〜
マイケル
マイケル
なお、シェーダについてはURP版のユニティちゃんトゥーンシェーダを使用させていただきました!
簡単にいい感じのトゥーンシェーダが作れるのでありがたいですね!
ScreenShot 2022 09 19 10 20 50↑ユニティちゃんトゥーンシェーダを使用

GitHub – UnityChanToonShaderVer2_Project – urp-2.5.1

エレキベア
エレキベア
URP版も出していただけていたクマね

アニメーションの作成

マイケル
マイケル
アニメーションは下記のように設定しています。
ぐちゃぐちゃで大変見苦しいですが、感情による動きの変化と石を置くアニメーション、結果のアニメーションをそれぞれ作っています。
ScreenShot 2022 09 19 10 22 09↑アニメーション設定
ScreenShot 2022 09 19 10 22 43↑作成したアニメーション
マイケル
マイケル
これをキャラクターの数だけ作成しました・・・。
エレキベア
エレキベア
これを7種類作るクマか・・・
えっぐ・・・
マイケル
マイケル
各キャラクターのアニメーションはAnimator Override Controllerとして作成しておけば、初めに作ったAnimator Controllerをベースに作成することができるためおすすめです。
なお、アニメーションはVery Animationを使用して作成しました。
ScreenShot 2022 09 19 10 23 01↑Animator Override Controllerとして作成
エレキベア
エレキベア
オーバーライドして作成できたのクマね〜〜
マイケル
マイケル
アニメーションのパラメータは下記のように受け口を作って設定するようにしました。
これでアニメーション設定は完了です!!
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に描画するよう設定しました。
ScreenShot 2022 09 19 10 28 58↑キャラクターをUI上に表示
ScreenShot 2022 09 19 10 28 48↑モデル自体は3D空間上に配置
マイケル
マイケル
Render Textureとカメラ、UIの設定は下記のようにしています。
カメラのCulling Maskにレイヤーを設定することで特定のレイヤーのみ表示させることができます。
ScreenShot 2022 09 19 10 24 52↑Render Textureの設定
ScreenShot 2022 09 19 10 27 58↑カメラの設定
ScreenShot 2022 09 19 10 29 45↑UIの設定
マイケル
マイケル
作成したプレイヤーへのレイヤー設定と、カメラワークの調整は下記のようにしました。
アタッチしたオブジェクトの全ての子オブジェクトに対してレイヤーを設定するようにしています。
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);
            }
        }
    }
}
↑子オブジェクト全てにレイヤーを設定する拡張実装
エレキベア
エレキベア
これでプレイヤー描画もバッチリクマね

キャラクターの選択処理

マイケル
マイケル
最後におまけとして、キャラクター選択画面のカメラ切り替えについても紹介します!
下記のように選択したキャラクターの前にカメラが移動するよう実装しました。
02 select↑キャラクター選択のカメラワーク
ScreenShot 2022 09 19 10 34 01↑タイトル画面ではキャラクターを全て並べている
マイケル
マイケル
こちらはCinemachineを使用しましたが、地道にカメラを置いて、
キャラクター選択時にそれぞれのカメラに切り替えることで実現しています。
ScreenShot 2022 09 19 10 34 33↑各キャラクターのカメラ
ScreenShot 2022 09 19 10 34 47↑各キャラクターに応じたカメラの設定
マイケル
マイケル
切替処理は下記の通りです!
種類に応じて切り替えるだけのシンプルなクラスになっています。
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周りや音源周りを作成すれば完成だ!
完成したらまた改めて記事にしようと思います!
マイケル
マイケル
それでは今日はこの辺で!
アデューー!!
エレキベア
エレキベア
クマ〜〜〜〜

【Unity】第五回 オセロAI開発 〜キャラクターらしさを表現したAIの作成〜【ゲームAI】〜完〜

コメント