- 投稿日:2020-09-07T22:33:41+09:00
【Unity】ボタンを押したらハート送りまくる機能を実装する【Particle System】
ボタンを1回押すと1つハートが出てきます。
【やってること】
・ボタンとParticle Systemを連携
・OnButtonDownを検知するたびにParticle SystemをPlay()【環境】
・Mac OSX El Capitan
・Unity versiton:2018.3.0【結果】
【作り方】
①Particle Systemを作成する
・Particle Systemをシーンに配置する
・Loopingのチェックを外す
・Start Speedの値を10にする
・PlayOnAwakeのチェックを外す
・EmissionのRate over Timeを0にする
・EmissionのBurstsを下図のように設定
※Burstsはパーティクルがどのタイミングでどれくらい出るかを設定できます。
今回は押した瞬間に1つ出てきて欲しいので、Time=0、Count=1です。
・Limit Velocity over Lifetimeを下図のように設定
※SpeedとDampenの役割についてはこちらのサイトを参考にさせていただきました
http://tsubakit1.hateblo.jp/entry/2017/05/03/211922
・Color over Lifetimeを下図のように設定
②Particle System用のMaterialを作る
・このハートのPNG画像をアセットに追加、Texture TypeをSprite(2D and UI)に変更する
※真っ白だから見えないけど↓ここにハートの画像があります。
・新しいMaterialを作成する
・ShaderをParticles/Standard Unitに変更する
・Render ModeをFadeにする
・ハートの画像をMaterialのAlbedoの□にドラッグ&ドロップする
・このMaterialをParticle SystemのRender内のMaterialに入れる③押したらハートが出るボタンを作る
・Buttonをシーンに配置する
・FlashLike.csをアタッチするFlashLike.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class FlashLike : MonoBehaviour { public ParticleSystem likeEffect; // Start is called before the first frame update void Start() { likeEffect = likeEffect.GetComponent<ParticleSystem>(); } public void PlayLikeEffect() { likeEffect.Play(); } }・InspectorからFlashLikeコンポーネントのLikeEffectにParticle Ststemを代入する
・ButtonコンポーネントのOnClickからFlashLike,PlayLikeEffectを選択する
以上!完成!
Limit Velocity over LifetimeやColor Over Lifetimeの値をいじって好きなハートの出し方を探ってみてください!
- 投稿日:2020-09-07T21:27:09+09:00
【Unity】ニコニコ動画みたいなコメント弾幕機能を作る
入力したコメントが右から左へ流れていくアレを実装します。
【環境】
・Mac OSX El Capitan
・Unity versiton:2018.3.0【結果】
【作り方】
まずはInputFieldを作成します。
InputFieldコンポーネントのLineTypeをMultiLineNewLineにします。
※SingleLineのままだと日本語入力ができないので注意!
こちらを参考にさせていただきました。
http://chnr.hatenablog.com/entry/2015/03/06/011559次にシーンにTextオブジェクトを作成します。
作成したTextオブジェクトの名前をTextPrefabに変更して下記スクリプトをアタッチします。TextPrefab.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class TextPrefab : MonoBehaviour { private Vector2 startPos; private RectTransform rectTransform; public float speed; // Start is called before the first frame update void Start() { rectTransform = this.gameObject.GetComponent<RectTransform>(); float height = Screen.height; float MaxHeight = height / 2; float MinHeight = -MaxHeight; float width = Screen.width; float textHeight = rectTransform.sizeDelta.y; float textWidth = rectTransform.sizeDelta.x; //最初の位置を画面右端にする startPos = new Vector2(width / 2 + textWidth/2, Random.Range(MinHeight + textHeight/2, MaxHeight + textHeight / 2)); rectTransform.localPosition = startPos; } // Update is called once per frame void Update() { //speedに応じて画面右から左へ流れていく transform.Translate(-speed * Time.deltaTime, 0, 0); //画面外へ出た場合は自身を削除する if (transform.localPosition.x < -Screen.width / 2 - rectTransform.sizeDelta.x/2) { Destroy(this.gameObject); } } }TextコンポーネントのRaycastTargetのチェックを外します。
※チェックがついたままだと、テキストがボタンの上を通過する際にボタンが押せなくなるので注意!
シーン上のTextPrefabをプレハブ化し、シーンに残っている方は削除します。そして、Buttonオブジェクトを作成し、下記のGenerateText.csをアタッチします。
GenerateText.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class GenerateText : MonoBehaviour { public GameObject textPrefab; private Text text; public InputField inputField; public float speed = 200; // Start is called before the first frame update void Start() { text = textPrefab.GetComponent<Text>(); inputField = inputField.GetComponent<InputField>(); } public void GenerateTextPrefab()//テキストプレハブのテキストにインプットフィールドのテキストを代入して生成し、スピードを設定する。 { text.text = inputField.text; GameObject newTextObj = (GameObject)Instantiate(textPrefab, transform.parent); newTextObj.GetComponent<TextPrefab>().speed = speed; } }InspectorでTextPrefab,InputFieldを代入。
Speedは任意の値を入れてください。
(だいたい200以上がいい感じです)ButtonコンポーネントのOnClick()の+ボタンをクリックして新しい項を作成し、自身のButtonオブジェクトを入れます。
プルダウンからGenerateText/GenerateTextPrefabを選択します。
これでボタンを押すとTextPrefabが生成される機能が実装されました。本家のディテールに合わせるなら、文字数に合わせて速度が変えるとよさそう。
あとは文字数に合わせてTextPrefabのサイズを変更するとベター。
実装したら追記します。
- 投稿日:2020-09-07T15:42:10+09:00
Unity ML-Agentsでターン制ゲームを作る
機械学習を使って、人間対機械で対局可能なリバーシを作りました。
前提
環境
- Unity 2019.4.9f1 (LTS)
- ML Agents 1.3.0 (Preview)
- ML Agents Extension 0.0.1 (Preview)
- Anaconda Python 3.8
- Windows 10
記事の範囲
- ML-Agentsをターン制ゲームへ応用できることを実証します。
- 具体的には、人間対機械で対局可能なリバーシを作成します。
- リバーシの戦術については、この記事では扱いません。
- 筆者は、今回初めてリバーシのルールを正確に把握しました。
- セオリーとして知っているのは四隅や四辺を取ると有利という程度のド素人です。
- ML-Agentsの基礎については、この記事では扱いません。
- 下敷きとなる記事 ⇒ 「Unity ML-Agentsを試してみた」
- リソースについても、上の記事を参照してください。
このプロジェクトで扱うリバーシのルール
- 8×8マスの正方形の盤を使用して、黒と白に分かれ石を交互に置いていきます。
- 黒が先手です。
- 盤中央には、あらかじめ、各色2個(計4個)の石を市松配置で置いておきます。
- 新たに置かれる石と同色の石に直線上で挟まれた他色の石は色が反転します。
- 縦横斜め方向に複数同時に挟むことができます。
- 挟んで反転可能な石のあるマスにしか石を置けません。
- 置けるマスがない場合は手番がパスされて相手の手番になります。
- 両者とも石が置けるマスがなくなったら終局となり、色の多い方が勝者となります。
学習環境の設計
- マスの基礎状態として「空、黒、白」の3値を正規化し、8×8=64マスをリニアに並べて(0~63)観測させます。
- マスの状態を5値(空、黒、白、黒可、白可)として観測させる方法も考えられます。
- 離散アクションスペースとして、石を置くマスのリニアインデックス(0~63)を使います。
- ルール的に置けないマスをマスクして、行動の無駄を省きます。
- 行動を制限せず、置けないマスを選んだらマイナス報酬を与えて、ルールから学習させる方法も考えられます。
- 一手ごとに微かな報酬を加え、勝利した場合は最大報酬、敗北した場合は最低報酬に置き換えます。
- 「強化学習の報酬は、結果に対して与える」ものなので、取ったマスの価値や一時的なスコアは考慮しません。
アプリの概略設計
目的
- ML-Agentsのターン制ゲームへの応用を検証します。
- そのため、ユーザを楽しませるための機能は実装しません。(リバーシ自体は楽しめます。)
クラス構成
- 論理層
namespace ReversiLogic
- 論理ゲーム
class Reversi
- 論理盤面の制御、ターンの制御、対局の制御
- 論理盤面
class Board
- 論理マスの制御、マスの状態(空、黒、白、黒可、白可)の判定、局面(黒可、白可、終局)の判定、ルールの制御
- 論理マス
class Square
- マスの基礎状態(空、黒、白)の制御、石を設置した順序の記録
- 物理層
namespace ReversiGame
- ローダー
class Loader
- 物理ゲームの構築と制御、モードの制御、コマンドラインの処理
- 物理ゲーム
class Game
- 物理盤面の構築と制御、ゲームの制御、エージェントとの通信
- 物理盤面 'class BaordObject'
- 物理マスの構築と制御、ステータス表示、オプションUIの制御
- 物理マス
class SquareObject
- 論理マス状態の表示、クリックの受付
- 確認ダイアログ
class Confirm
- 再初期化などの確認UIの制御
- リバーシエージェント
class ReversiAgent
Unity.MLAgents.Agents
のサブクラス段階的な実装
- 論理層を実装して、ログ出力によって動作を確認します。
- 物理層を実装して、論理層の挙動をゲーム画面で確認可能にします。
- 物理層を拡張して、人間対人間プレイの挙動を確認可能にします。
- エージェントを実装して、機械対機械プレイを可能にします。
- 学習を実施します。
- 学習結果を取り込んで、人間対機械プレイを可能にします。
- スコアの表示や手番の選択など、人間対機械プレイ用のUIを整備します。
- 思考の練度を評価し、トレーニングの構成を試行錯誤します。
実装された仕様
機能
- 人間対機械(プレイモード)と機械対機械 (トレーニングモード)のみで、選択UIはありません。
- 起動時にモードを切り替えられます。 (コマンドライン引数)
- 内部機構的には人間対人間も可能です。 (エージェントのパラメータを双方とも
BehaviorType.HeuristicOnly
に固定)- プレイモード
- 先手/後手の選択が可能です。 (UIで選択)
- 次にコマの置けるマスと、コマを置いた順番が表示可能です。 (UIで切り替え)
- トレーニングモード
- 画面を更新しません。 (UIで切り替え)
- 起動時に使用する盤面数を指定できます。 (コマンドライン引数)
- 次の手番、最後に置いたコマ、現在のコマ数、累積勝敗数が随時表示されます。
- 棋譜は記録せず、「待った」はできません。
コマンドライン引数
-trainer
- トレーニングモードで起動します。デフォルトはプレイモードです。
-width <整数>
- トレーニング時に横に並べる盤面の数です。デフォルトは
7
です。-height <整数>
- トレーニング時に縦に並べる盤面の数です。デフォルトは
7
です。なお、エディタでトレーニングする場合のために、
TRAINER_TEST
シンボルで、コマンドライン引数のシミュレーションが可能です。思考の評価
- 私は、勝てることもありますが、負け越してます。(弱すぎて評価不能)
- 複数の難易度が選べる無料アプリと対局させたところ、最弱レベルに辛勝する程度でした。(弱い)
エージェントのコードと解説
class ReversiAgent
リバーシ・エージェント
- これは、リバーシの思考を担う
Agent
のサブクラスです。
Agent
はMonoBehaviour
のサブクラスです。- 一つの物理ゲームに、黒と白を担当する二つの
ReversiAgent
インスタンスが作られます。- 本来は、エージェント自身がゲームの進行を制御するのでしょうが、このプロジェクトでは外部で進行が制御され、エージェントは思考のみを担うようになっています。
ReversiAgent.csusing System.Collections.Generic; using System; using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; using Unity.MLAgents.Policies; using ReversiLogic; namespace ReversiGame { /// <summary>リバーシ・エージェント</summary> public class ReversiAgent : Agent { /// <summary>チーム識別子</summary> public enum TeamColor { Black = 1, White = 0, } /// <summary>盤面のマス数</summary> private const int Size = ReversiLogic.Board.Size; /// <summary>物理ゲーム</summary> private Game game = null; /// <summary>論理ゲーム</summary> private Reversi reversi => game.Reversi; /// <summary>挙動パラメータ</summary> private BehaviorParameters Parameters => _parameters ?? (_parameters = gameObject.GetComponent<BehaviorParameters> ()); private BehaviorParameters _parameters; /// <summary>チームID</summary> public int TeamId => Parameters.TeamId; /// <summary>チーム</summary> public TeamColor Team { get; private set; } /// <summary>黒である</summary> public bool IsBlack => Team == TeamColor.Black; /// <summary>白である</summary> public bool IsWhite => Team == TeamColor.White; /// <summary>自分が優勢</summary> public bool IWinner => (Team == TeamColor.Black && reversi.BlackWin) || (Team == TeamColor.White && reversi.WhiteWin); /// <summary>自分が劣勢</summary> public bool ILoser => (Team == TeamColor.Black && reversi.WhiteWin) || (Team == TeamColor.White && reversi.BlackWin); /// <summary>挙動タイプ</summary> public BehaviorType BehaviorType { get => Parameters.BehaviorType; set => Parameters.BehaviorType = value; } /// <summary>人間が操作</summary> public bool IsHuman { get => (Parameters.BehaviorType == BehaviorType.HeuristicOnly); set { if (reversi.Step == 0 || reversi.IsEnd) { Parameters.BehaviorType = value ? BehaviorType.HeuristicOnly : BehaviorType.InferenceOnly; } } } /// <summary>機械が操作 (推論時のみ)</summary> public bool IsMachine { get => (Parameters.BehaviorType != BehaviorType.HeuristicOnly); set { if (reversi.Step == 0 || reversi.IsEnd) { Parameters.BehaviorType = value ? BehaviorType.InferenceOnly : BehaviorType.HeuristicOnly; } } } /// <summary>チームカラーの入れ替え (チームIDは変更しない)</summary> public bool ChangeTeam () { if (reversi.Step == 0 || reversi.IsEnd) { Team = (Team == TeamColor.Black) ? TeamColor.White : TeamColor.Black; return true; } return false; } /// <summary>人間と機械の入れ替え</summary> public bool ChangeActor () { if ((reversi.Step == 0 || reversi.IsEnd) && Parameters.BehaviorType != BehaviorType.Default) { Parameters.BehaviorType = (Parameters.BehaviorType == BehaviorType.HeuristicOnly) ? BehaviorType.InferenceOnly : BehaviorType.HeuristicOnly; return true; } return false; } /// <summary>オブジェクト初期化</summary> private void Awake () => Init (); /// <summary>初期化</summary> public void Init () { if (!game) { // 一度だけ game = GetComponentInParent<Game> (); Team = (TeamColor) TeamId; } } /// <summary>エピソードの開始</summary> public override void OnEpisodeBegin () { Debug.Log ($"OnEpisodeBegin ({Team}): step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); if (reversi.Step > 0 && game.State == GameState.Play) { Debug.LogError ("Not Reseted"); } } /// <summary>環境の観測</summary> public override void CollectObservations (VectorSensor sensor) { Debug.Log ($"CollectObservations ({Team}): step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); for (var i = 0; i < Size * Size; i++) { sensor.AddObservation ((float) reversi [i].Status / (float) SquareStatus.MaxValue); // 正規化 } } /// <summary>行動のマスク</summary> public override void CollectDiscreteActionMasks (DiscreteActionMasker actionMasker) { Debug.Log ($"CollectDiscreteActionMasks ({Team}): step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); var actionIndices = new List<int> { }; for (var i = 0; i < Size * Size; i++) { var status = reversi.Status (i); if ((IsBlack && !status.BlackEnable ()) || (IsWhite && !status.WhiteEnable ())) { // 自分が置けない場所 actionIndices.Add (i); } } actionMasker.SetMask (0, actionIndices); } // 例外 public class AgentMismatchException : Exception { } public class TeamMismatchException : Exception { } /// <summary>行動と報酬の割り当て</summary> public override void OnActionReceived (float [] vectorAction) { Debug.Log ($"OnActionReceived ({Team}) [{vectorAction [0]}]: step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); if (IsMachine) { try { if (game.TurnAgent != this) throw new AgentMismatchException (); // エージェントの不一致 if ((reversi.IsBlackTurn && Team != TeamColor.Black) || (reversi.IsWhiteTurn && Team != TeamColor.White)) throw new TeamMismatchException (); // 手番とチームの不整合 var index = Mathf.FloorToInt (vectorAction [0]); // 整数化 if (!reversi.Enable (index)) { throw new ArgumentOutOfRangeException (); } // 置けない場所 game.Move (index); Debug.Log ($"Moved ({Team}) [{index}]: step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); AddReward (-0.0003f); // 継続報酬 } catch (AgentMismatchException) { EndEpisode (); Debug.LogError ($"Agent mismatch ({Team}): Step={reversi.Step}, Turn={(reversi.IsBlackTurn ? "Black" : "White")}, Status={reversi.Score.status}\n{reversi}"); } catch (ArgumentOutOfRangeException) { EndEpisode (); Debug.LogWarning ($"DisableMove ({Team}) [{vectorAction [0]}]: step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}\n{reversi}"); } catch (TeamMismatchException) { Debug.LogWarning ($"Team mismatch ({Team}): Step={reversi.Step}, Turn={(reversi.IsBlackTurn ? "Black" : "White")}, Status={reversi.Score.status}\n{reversi}"); } finally { game.TurnAgent = null; // 要求を抹消 } } else { Debug.LogError ($"{Team}Agent is not Human: step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); } } /// <summary>終局処理と最終的な報酬の割り当て</summary> public void OnEnd () { Debug.Log ($"AgentOnEnd ({Team}, {(IWinner ? "winner" : ILoser ? "loser" : "draw")}): step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); if (IsMachine) { if (IWinner) { SetReward (1.0f); // 勝利報酬 } else if (ILoser) { SetReward (-1.0f); // 敗北報酬 } else { SetReward (0f); // 引き分け報酬 } EndEpisode (); } else { Debug.LogError ($"{Team}Agent is not Human: step={reversi.Step}, turn={(reversi.IsBlackTurn ? "Black" : "White")}, status={reversi.Score.status}"); } } } }
using ReversiLogic;
論理ゲーム
- これは、論理ゲームの名前空間です。
class Reversi
、class Board
、class Square
などが含まれます。- ここに含まれるクラスは
MonoBehaviour
を継承せず、Unityに依存しません。- リバーシのルールや盤面の状態などは、全てこの中にあります。
namespace ReversiGame
物理ゲーム
- これは、物理ゲームの名前空間で、画面表示とUIが含まれます。
class Loader
、class Game
、class Board
、class Square
などが含まれます。- ここに含まれるクラスは
MonoBehaviour
を継承したUnityコンポーネントです。
enum TeamColor
チーム識別子
- ML-Agentsでは、複数のチームに分かれて対戦や対局が可能なように、エージェントの挙動パラメータに
TeamId
という整数値があります。- このプロジェクトでは、黒のチームと白のチーム(各ひとつのエージェント)があります。
- トレーニング時、先手と後手それぞれの担当エージェントの
TeamId
は固定されています。これは、バックエンド側で担当チームの交代が行われることを前提にしているためです。- これは、
TeamId
の数値に合わせたシンボルです。
ChangeTeam ()
チームカラーの入れ替え (チームIDは変更しない)
- このプロジェクトでは使用していません。
- これは、先手と後手(黒と白)を交代する仕組みのひとつで、
TeamId
の担当する色を入れ替えます。
- 「エージェント0 = 白担当、エージェント1 = 黒担当」⇒「エージェント0 = 黒担当、エージェント1 = 白担当」
TeamId
とTeamColor
が一致しない状態が生じます。
ChangeActor ()
人間と機械の入れ替え
- これは、人間と機械の担当する色を入れ替える仕組みで、エージェントの挙動タイプを入れ替えます。
- 「エージェント黒 = 人間担当、エージェント白 = 機械担当」⇒「エージェント黒 = 機械担当、エージェント白 = 人間担当」
- エージェントの挙動タイプは、人間操作(
HeuristicOnly
)、機械推論(InferenceOnly
)、機械学習(と推論の自動切り替えDefault
)の別です。
Init ()
初期化
- 一度だけ必要な初期化を行います。
- 外部からの使用前に明示的に呼んでいますが、一応、
Awake ()
からも呼ばれます。
OnEpisodeBegin ()
エピソードの開始
- このプロジェクトでの「エピソード」は「一局」に相当します。
- 開始していない状態で、行動の決定を要求(
RequestDecision ()
)されると、最初に呼び出されます。- あるいは、エピソードの終了(
EndEpisode ()
)を行うと中で呼ばれます。- 本来はここでゲームを初期化するのですが、このプロジェクトは外部で初期化するため、チェックを行うのみで何もしていません。
CollectObservations ()
環境の観測
- 行動の決定を要求(
RequestDecision ()
)される毎に呼ばれます。- 単純に盤面の8×8マスの石の配置状態を観測させます。
CollectDiscreteActionMasks ()
行動のマスク
- 環境の観測後に呼ばれ、選択不能な行動の選択肢をエージェントに示唆します。
- ここでは、ルール上、石を置けないマスをマスクしています。
OnActionReceived ()
行動と報酬の割り当て
- 決定された行動に基づいて実際に石を打ち、その結果に応じた報酬を出します。
- このプロジェクトでは、行動(一手)は単一のスカラー値、中身はマスのインデックス(
0
~63
の整数値)で表されます。- 行動を整数値で生成するために、エージェントの挙動パラメータ
VectorAction SpaceType
に対してDiscrete
を指定しています。- 処理のほとんどはエラーチェックで、作用としては、決定された行動「石を置く位置(マスのインデックス)」を論理ゲームに渡し、微量の継続報酬を加算するだけです。
- この継続報酬は、終局に至ると上書きされて捨てられます。
- 本来は、終局の判定と勝敗に対する報酬の支払い、エピソードの終了などを、ここで処理するのですが、このプロジェクトでは
OnEnd ()
に分離しています。
- 人間の操作を外部で受け付けて処理している関係で、独立して呼び出せるようにしてあります。
OnEnd ()
終局処理と最終的な報酬の割り当て
- 本来は、
OnActionReceived ()
の中で行われる処理ですが、人間の操作を外部で受け付けて処理している関係で分離されています。- 終局を判定した際に外部から呼び出されます。
- ここでの報酬は、それまでの報酬を上書きして、単純に勝てば
1
、負ければ-1
になります。
Heuristic ()
人間の入力
- このプロジェクトでは、このメソッドを実装しません。
- 人間の操作は、エージェントの外で制御されています。
ReversiAgent
のBehaviorParameters
トレーニングの構成
ReversiConfig.yamlbehaviors: Reversi: trainer_type: ppo hyperparameters: batch_size: 32 buffer_size: 20480 learning_rate: 3e-4 beta: 5.0e-3 epsilon: 0.2 lambd: 0.95 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 512 num_layers: 3 vis_encode_type: simple reward_signals: extrinsic: gamma: 0.99 strength: 1.0 keep_checkpoints: 5 max_steps: 500000 time_horizon: 48 summary_freq: 10000 threaded: false self_play: save_steps: 20000 team_change: 100000 swap_steps: 10000 window: 10 play_against_latest_model_ratio: 0.5 initial_elo: 1200.0課題
以下のような課題が生じ、対処済み、あるいは、対処中です。
脈略なく
OnEpisodeBegin ()
が呼ばれる
- 本来の学習サイクルは以下のようなものです。
- ターンが廻ってきたエージェントに決定要求
RequestDecision ()
を行う- エピソードが開始されていない場合、エピソードの開始
OnEpisodeBegin ()
が呼ばれる
- ここで、対局の初期化を行う
- 環境の観測
CollectObservations ()
が呼ばれる- 行動のマスク
CollectDiscreteActionMasks ()
が呼ばれる- 行動と報酬の割り当て
OnActionReceived ()
が呼ばれる
- 終局していたらエピソードの終結
EndEpisode ()
を行う- しかし、ML-Agentsのバックエンド側からの制御で、サイクルの途中で
OnEpisodeBegin ()
が呼ばれる場合があります。
- 既に
RequestDecision ()
が行われていると、過去のエピソードに対するOnActionReceived ()
が呼ばれます。- 盤面の初期化後に、過去の局面に対しての行動要求が届くので、不整合が生じます。
対処
config.yaml
のthreaded
をfalse
にすれば、学習サイクル中の割り込みはなくなります。弱い
- 私が勝てるのだから弱いのは間違いありません。
対処
TensorBoard
でモニターしています。- トレーニングの構成を見直しています。(研究中)
検討中
以下の公開については検討中です。(全てのリンクは無効です。)
アセットの入手 (GitHub)
ダウンロード ⇒ mla-Reversi.unitypackage
ソースはこちらです。Android App
- 投稿日:2020-09-07T08:47:00+09:00
Chrome拡張でマスタデータ管理ツールを作ったら開発に変化が起きた話(ツール公開しました)
よくある運用
マスタデータの管理方法として多いのは、Excelなどの表計算ソフトを使ってデータを作成し、それをGitHubで管理しつつ、実際にDBなどへデプロイさせる方法かと思われます。
そしてこれを実現するにあたって、出来るだけエンジニアの負担を減らせるよう、データ作成者にGitクライアントの使い方やデプロイ方法をレクチャーします、が...
マスタデータ管理ツールの誕生
ただ、そこで気になるのは、結果的にデータ作成者の負担が増えてしまうことです。
大体の場合、表計算ソフト以外のツールも触ることになり、もう少し手軽にできないかと考えていました。そこで今回は、Chrome拡張とSpreadsheetを用いて、データ作成者がChromeだけでデータの作成から反映までできるツールを作成しました。
少し前から、Spreadsheetでデータを作成後にデプロイツール実行一つでデータを反映する、といった事例も出てきて、「ならこのデプロイツールをChrome拡張にすればさらに手軽になるのでは」という発想を元に誕生しています。
今回作成および公開したのはSSBirdというツールです。
https://github.com/yukiarrr/SSBirdSSBirdでは、DBへのデプロイなど各プロジェクトの環境に依存してしまう部分を除き、データを作成してGitHubにプッシュするまでの工程を負担します。
(プッシュ後に各PJのCI/CDでデプロイされる想定)立ち位置としては、Spreadsheetに最適化されたGitクライアントです。
また、複数人での並行作業やCIでのデータチェックを考慮して、シート同士のマージ機能やPull Requestを作成する機能も備えています。
(上記のgifでは、developシートにadd-dataシートの内容をマージ)
マスタデータの管理に関してはデファクトスタンダードになるようなものがなく、各プロジェクトが毎回独自でその仕組みを作っている印象が強いです。
なので、その負担を出来るだけ減らす手段の一つになれればという思いから公開しました。マスタデータ管理ツールについて
今回作成したマスタデータ管理ツールの説明です。
もし先に導入してどんな変化が起きたのか知りたい方はこちらから。特徴
SSBirdの特徴で、作成の際に意識しているのは以下の5つです。
- Chromeだけで完結
- シート同士のマージ機能
- 高速なシート操作
- 導入が容易
- 汎用性の高さと依存性の低さ
1. Chromeだけで完結
Git操作などを全てChrome拡張で補うため、データ作成者はChrome以外を触る必要がありません。
Spreadsheetでデータを作成して、その画面のままでChrome拡張から手軽にデータが反映できるため、とても高速な開発が可能になります。2. シート同士のマージ機能
データの作成は、そのプロジェクトの規模が大きくなるほど複雑になり、複数人で作業することが多くなってきます。しかしその場合、表計算ソフトのような一つのデータを編集する仕組みだと、コンフリクトが発生しやすくなってしまいます。
そこで用意したのが、シート同士のマージ機能です。
この機能を使えば、一つのシートに別のシートのデータを追加・上書きできます。
チームでマージ先のシートを決めて、そこを直接編集しないようにルール付けしておけば、コンフリクトが起こりにくくなります。
(GitHub Flowに近いですね)3. 高速なシート操作
Spreadsheetのシートを操作するAPIは何種類か存在しますが、その一つ一つをパフォーマンス検証しながら実装し、セル単位で操作対象を絞り込むことで、マージやcsv変換などシート操作にかかる時間を最小化しました。
これにより、出力するcsvが数MBにもなるシートへのマージも、数秒単位で完了するようになりました。
結果として「1. Chromeだけで完結」も相まって、データ作成者が手軽かつ高速にデータ反映ができるようになります。4. 導入が容易
このツールはバックエンドとして必要な機能をGASでまかなっているため、導入の際にサーバーを立てる必要がありません。
また、表計算ソフトとしてはSpreadsheetを採用しているため、Googleアカウント一つあれば導入することができます。5. 汎用性の高さと依存性の低さ
このツールはシート同士をマージしGitHubにプッシュしているにすぎません。
そのため、各プロジェクトごとでシート運用をカスタマイズできるという、汎用性の高さを持ち合わせています。そしてそれは裏を返せば、SSBirdへの依存性が低く、他のツールへの代替も容易ということになり、「4. 導入が容易」も相まって、導入のハードルが比較的低いものになっていると考えています。
導入方法
具体的な導入方法は、READMEに記述しているので、そちらをご覧ください(日本語版あります)。
使い方
ここでは、開発ブランチ名を「develop」と定め、このブランチにcsvがプッシュされるとCI/CDで開発環境にデータが反映されるものとします。
最も手軽にデータを反映する方法
最も手軽なのは、開発ブランチに相当するシートを直接編集する方法です。
後述しますが、Spreadsheet内のシート名がプッシュするブランチ名となるため、ここではdevelopシートを編集することになります。
データ作成後、Spreadsheet上でSSBirdを開きます。
「Target Sheet」が対象となるシートなので、ここに「develop」を指定します。
この状態でApplyすると、developブランチが更新され、データが反映されます。シート同士をマージして反映する方法
上記の方法だと、developシートを直接編集するため、複数人データ作成者が存在すると、コンフリクトし互いに予期せぬデータになってしまう可能性があります。
そこで利用するのが、シート同士のマージ機能です。
マージの仕組みは後述しますが、「Target Sheet」の他にマージ用のシートを「Merge Sheets」に指定します。
なお、この「Merge Sheets」に関しては複数シート指定可能で、選択順にマージされていきます。この状態でApplyすると、add-dataシートの内容がdevelopシートに追加・上書きされ、それがdevelopブランチにプッシュされます。
複数のSpreadsheetのデータを反映する方法
ここまでの方法は、全てSpreadsheet一つに対しての処理でしたが、Spreadsheetの存在するDrive上でSSBirdを開くと、複数のSpreadsheetに対して処理できるようになります。
このように、「Apply Spreadsheets」が選択可能になるので、ここへ反映したいSpreadsheetを選択します。
なお、「Target Sheet」などは上記の方法と同じ仕様で、選択したSpreadsheet全てに対して適応されます。なお注意点として、Spreadsheet上ではないため「Target Sheet」などの候補が出ません。
使用者自身で入力する必要があります。Pull Requestを作る方法
データの量が多くなってくると、どうしてもヒューマンエラーによるデータの不整合が起こりがちとなってしまいます。
この対策としてよく挙げられるのが、Pull Request上のCIによるデータチェックです。SSBirdでもそちらを考慮し、シートをマージする代わりにPull Requestを作成する機能が存在します。
普段通り「Target Sheet」と「Merge Sheets」を指定したあと、「Create Pull Request」にチェックを入れてからApplyします。
すると、通常であれば「Target Sheet」のシートが更新されプッシュされるはずが、シートには更新がかからず「Merge Sheets」の最後に選択したシート名のブランチがプッシュされPull Requestが作成されます。
このPRでの変更ファイルは、本来シートが更新されプッシュされるはずだったものになっています。
これにより、PRベースでの開発が可能となります。なお注意点として、「Target Sheet」に指定したシートは更新されていないので、PRがマージされた際に、CLI版の方でSpreadsheet側を更新するようにしておくことをお勧めします。
Spreadsheetでのルール
SSBirdではSpreadsheetを用いてデータを作成しますが、その際に意識していただくルールは3つあります。
- Spreadsheet名がファイル名、シート名がブランチ名となる
- コメントアウト
- A列のセルとカラムを基準にマージ
1. Spreadsheet名がファイル名、シート名がブランチ名となる
Spreadsheet名がexampleならGitHub上ではexample.csvとなり、対象となるシート名がdevelopならcsvはdevelopブランチにプッシュされます。
2. コメントアウト
A列またはカラムが空白なセルは、マージ・csv変換どちらにおいても無視されます。
具体的には、
こちらのシートがマージ・csv変換される際、黄色い部分以外はコメントとみなされ無視されます。
これを利用して、一時的にコメントアウトするためにA列のみを空にしたり、メモを残すことができます。3. A列のセルとカラムを基準にマージ
A列のセル(A1,A2,A3...)が同じであれば同じデータとみなし上書き、違えば新しいデータとして最下位に追加されます。
データが上書きされる場合、カラムを基準として上書きするので、A列のカラム以外の順番が対象となるシートとマージ用のシートでバラバラでも問題ありません。また、マージ用のシートに、対象となるシートにない新しいカラムがあれば、新しくカラムが追加されます。
CLI版
SSBirdの機能をCI/CD上やJobで使いたい方のために、CLI版も用意しています。
CLI版にはChrome拡張にはない、GitHub側のcsvデータをSpreadsheetに反映する機能も有しています。https://github.com/yukiarrr/SSBird/cli/ssbird
以下のコマンドでインストールできます。
go get -u github.com/yukiarrr/SSBird/cli/ssbird
インストール後に、CLI版はGoogleとの認証が必要になりますが、その手段としてOAuth 2.0とService Accountの2種類を用意しています。
なお、このCLI版に関しては現在開発中で、まだ機能が揃っていないのでご注意ください。
使われている技術
SSBirdでは、以下の3つの言語を利用しています。
- JavaScript
- Google Apps Script
- Go
具体的な説明は別でできればと思いますので、ここでは軽めに紹介します。
JavaScript
クライアントサイド、つまりChrome拡張で利用します。
また、UIのドロップダウンの箇所には、selectize.jsを活用させていただいています。Google Apps Script
シートの操作を行っています。
具体的には、GASでシートを操作する箇所を記述して、それをWebアプリケーションとして公開し、クライアントサイドから呼び出しています。Go
Chrome拡張だけではGitの操作ができないので、Native Messagingの機能を使って、バイナリ実行しています。
そのバイナリの中身をGoで記述しており、go-gitを活用してGitを操作しています。何が変わったのか
ここでは自身のプロジェクトでの出来事を事例として紹介します。
クライアントはUnityで、APIを介してサーバー側と通信を行うオーソドックスな構成として認識していただけると幸いです。変化前
マスタデータ管理ツールの導入前、自身のプロジェクトではデータ作成者がExcelでデータを作成し、それをエンジニアが受け取ってDBなどにデプロイしていました。
ただ、この方法だと、
- エンジニアの負担が増え、開発が遅れる
- デプロイまで時間がかかり試行回数が減ってしまう
- 結果、データに不備が見つかりがちになる
といった問題が発生してしまいます。
これにより、データ量が膨らみそうな施策が検討され始めたタイミングで、マスタデータの作成フローが整備されることになりました。とはいえ、このプロジェクトが新規だったこともあり、そこまで大きく時間を費やせるわけではありません。
そこで、自分が個人で制作していたSSBirdを候補の一つとして検証することになりました。変化後
SSBirdは候補としての導入でしたが、結果は良好でした。
- データの作成から反映をChromeだけで行える
- 結果、Gitクライアントを使わずにGit管理できる
- エンジニアはプッシュ後のCI/CDを整備するだけで良く、またこれ自体はSSBirdを見送っても無駄にならない
こういった点などから評判が良く、時間に余裕がない新規案件などでは相性が良いように感じられました。
そして結果的にSSBirdが採用されることになりましたが、その後しばらくして、プロジェクト全体である変化が起き始めていました。それが「職種間の超越」です。
これの要因として考えられるのが、「データの作成」と「データの反映」の壁がなくなったことです。
元々、データ作成者とエンジニアで役割が完全に別れていたものが、マスタデータのフロー整備によりゼロに近づき、またSSBirdの「データの作成から反映をChromeだけで行える」や「結果、Gitクライアントを使わずにGit管理できる」による手軽さが、それをさらに加速させていたように思われます。この結果、エンジニアもデータ作成に携わったり、データ作成者からも新しいシート運用を試行したりなど、プロジェクトの生産性が大きく向上したように感じています。
まとめ
マスターデータ管理ツール「SSBird」について紹介しました。
このツールを導入しても、残念ながらマスタデータの管理にかかる負担をゼロにすることはできません。
とはいえ、減らすことはできるかと考えているので、マスタデータのフロー整備の際、選択肢の一つとなれれば嬉しいと思っています。最後に、SSBirdは現在も開発中なので、興味があればGitHubの方をチェックしてみてください。
ありがとうございました!
https://github.com/yukiarrr/SSBird