ゲーム開発
Unity
UnrealEngine
C++
Blender
Houdini
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • Houdini
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20220919_01

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

      UnityアニメーションゲームAI
      2022-09-19

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

      ↑前回までの記事

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

      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種類で、それぞれの個性に合わせて打つ手を変えています。
      (調整次第で変更の可能性は有り)
      名前 通常 焦り 悲しい 補足
      ピカル 強化学習(弱) ランダム 強化学習(弱) 簡単に勝てるようにする。
      マイケル 強化学習(強) ランダム モンテカルロ 焦った時は慌ててランダム、負けそうな時は考える。
      エレキベア 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ファイルのみ差し替えることが可能になるためおすすめです!
      ↑作成したモデル
      エレキベア
      エレキベア
      これだけ作るとなるとかなり大変そうクマ〜〜
      (オセロでこんなに作る必要があったのクマか・・・)
      マイケル
      マイケル
      Blenderを使用して作成したけど、使い方を思い出しながらで大変だったよ・・・
      キャラモデル作成方法についても記事にしてありますので、よければこちらもご覧ください!

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

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

      GitHub – UnityChanToonShaderVer2_Project – urp-2.5.1

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

      アニメーションの作成

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

      キャラクターの選択処理

      マイケル
      マイケル
      最後におまけとして、キャラクター選択画面のカメラ切り替えについても紹介します!
      下記のように選択したキャラクターの前にカメラが移動するよう実装しました。
      ↑キャラクター選択のカメラワーク
      ↑タイトル画面ではキャラクターを全て並べている
      マイケル
      マイケル
      こちらは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周りや音源周りを作成すれば完成だ!
      完成したらまた改めて記事にしようと思います!
      マイケル
      マイケル
      それでは今日はこの辺で!
      アデューー!!
      エレキベア
      エレキベア
      クマ〜〜〜〜

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


      UnityアニメーションゲームAI
      2022-09-19

      関連記事
      【Unity】Timeline × Excelでスライドショーを効率よく制作する
      2024-10-31
      【書籍紹介】「コンピュータグラフィックス」に出てくる用語をまとめる【CGエンジニア検定】
      2024-07-13
      【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
      2024-05-28
      【Unity】GoでのランキングAPI実装とVPSへのデプロイ方法についてまとめる【Go言語】
      2024-04-14
      【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-30
      【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
      2024-03-30
      【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-28
      【Unity】第一回 CRI ADXを使用したサウンド制御 〜基本動作、周波数解析編〜
      2024-03-28