あかすくぱるふぇ

同人サークル「あかすくぱるふぇ」のブログです。

Unity

Unityでオセロ①(http://akasuku.blog.jp/archives/66717359.html)の続きです。
今回は単純なAIを実装しました。

クラス設計は以下のようにしました。
無題

前回の記事では黒番と白番の両方が人間でしたが、今回は人間とAIが交互に打ちます。
なので、PlayerBaseクラスを継承する形でHumanPlayerクラスとMachinePlayerクラスを作り、GameScriptクラスが持っていた「プレイヤーが石を置く」という役割をこれらのクラスに委譲しました。
さらに、これらのクラスからGridScriptクラスにアクセスする必要があったので、GameScriptクラスがBoardScriptクラスを保持する設計に変更し、PlayerBaseクラスからBoardScriptクラスへ関連を持たせました。

・PlayerBaseクラス
using UnityEngine;
using System.Collections;

abstract public class PlayerBase : MonoBehaviour
{
protected BoardScript boardScript_;
protected bool isBlack_;

public void SetBoardScript(BoardScript boardScript)
{
boardScript_ = boardScript;
}

public void IsBlack(bool isBlack)
{
isBlack_ = isBlack;
}

public abstract bool Play();

// Use this for initialization
void Start()
{
}

// Update is called once per frame
void Update()
{
}
}
・HumanPlayerクラス
using UnityEngine;
using System.Collections;

public class HumanPlayer : PlayerBase {

// タップを検知し、GridScriptを取得する
bool DetectTap(out GridScript gridScript)
{
gridScript = null;
if (Input.GetMouseButtonDown(0)) // タップ検知
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit = new RaycastHit();
if (Physics.Raycast(ray, out hit))
{
GameObject obj = hit.collider.gameObject; // タップしたGameObjectを取得
gridScript = obj.GetComponent<GridScript>(); // GridScriptを取得
return true;
}
}
return false;
}

public override bool Play()
{
// タップ検知
GridScript gridScript;
if (DetectTap(out gridScript))
{
// グリッドをタップしており、かつ、そこに石が無いなら
if (gridScript && !gridScript.GetStone())
{
// そこに石を置けるなら
if (gridScript.JudgeStonePutable(isBlack_))
{
// 石を置く
gridScript.PutStone(isBlack_);

// ひっくり返す
gridScript.TurnStone(isBlack_);

return true;
}
}
}
return false;
}

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

}
}
・MachinePlayerクラス
MachinePlayerクラスは各グリッドに石を置けるか否かを探索し、ラスタ順で最も最初のグリッドに石を置きます。
using UnityEngine;
using System.Collections;

public class MachinePlayer : PlayerBase
{
void SearchPutableGrid(out bool[] isPutableGrid)
{
int colNum = boardScript_.GetColNum();
int rowNum = boardScript_.GetRowNum();
isPutableGrid = new bool[colNum * rowNum];

for (int r = 0; r < rowNum; ++r)
{
for (int c = 0; c < colNum; ++c)
{
GridScript gridScript = boardScript_.GetGrid(c, r).GetComponent<GridScript>();
if (!gridScript.GetStone() && gridScript.JudgeStonePutable(isBlack_))
{
isPutableGrid[r * colNum + c] = true;
}
else
{
isPutableGrid[r * colNum + c] = false;
}
}
}
}

bool SelectGrid(out GridScript gridScript)
{
bool[] isPutableGrid;
SearchPutableGrid(out isPutableGrid);

int colNum = boardScript_.GetColNum();
int rowNum = boardScript_.GetRowNum();
for (int r = 0; r < rowNum; ++r)
{
for (int c = 0; c < colNum; ++c)
{
int index = r * colNum + c;
if (isPutableGrid[index])
{
gridScript = boardScript_.GetGrid(c, r).GetComponent<GridScript>();
return true;
}
}
}
gridScript = null;
return false;
}

public override bool Play()
{
// 石を置くグリッドを選ぶ
GridScript gridScript;
SelectGrid(out gridScript);
if (!gridScript) Application.Quit();

// 石を置く
gridScript.PutStone(isBlack_);

// ひっくり返す
gridScript.TurnStone(isBlack_);

return true;
}

// Use this for initialization
void Start()
{

}

// Update is called once per frame
void Update()
{
}
}
・GameScriptクラス
using UnityEngine;
using System.Collections;

public class GameScript : MonoBehaviour {

public GameObject gridPrefab_, blackStonePrefab_, whiteStonePrefab_;
BoardScript boardScript_;
PlayerBase[] players_;
bool isBlackTurn_;

// Use this for initialization
void Start () {
// BoardScriptクラスの生成
boardScript_ = new BoardScript();
boardScript_.SetGridPrefab(gridPrefab_);
boardScript_.SetBlackStonePrefab(blackStonePrefab_);
boardScript_.SetWhiteStonePrefab(whiteStonePrefab_);
boardScript_.MakeGrids();

// Playerクラスの生成
players_ = new PlayerBase[] {
new HumanPlayer(),
new MachinePlayer(),
};
players_[0].IsBlack(true);
players_[1].IsBlack(false);
for (int i = 0; i < 2; ++i)
{
players_[i].SetBoardScript(boardScript_);
}

isBlackTurn_ = true;
}

// Update is called once per frame
void Update () {
int playerNo = isBlackTurn_ ? 0 : 1;
if (players_[playerNo].Play())
{
isBlackTurn_ = !isBlackTurn_;
}
}
}

現状の実装だと、人間が黒石を押すと、その次の瞬間には白石が置かれてしまいます。
これの修正とAIを賢くするのが次の記事になるかと思います。

突然ですが、Unityでオセロ作ってます。
本記事では、その制作過程をご紹介します。

無題_

・Prefabの作成
下図のようなPrefabを作成します。
無題

・クラス設計
クラス設計は以下のようにしました。
BoardScriptがボード全体を、GridScriptがボードの中の各グリッドを、StoneScriptがグリッドに置かれる各石を表しています。
GameScriptがゲームを統括するクラスで、グリッドのタップを検知し、GridScriptに対して石を置いたり、ひっくり返したりといった処理をさせます。
GridScriptクラスが自分の周辺のグリッドの情報をBoardScriptクラスから得られるように双方向関連を持たせています。
無題_

・StoneScript
StoneScriptクラスは、自分が黒石か白石かという情報だけを扱います。
using UnityEngine;
using System.Collections;

public class StoneScript : MonoBehaviour {

bool isBlack_;

public bool IsBlack()
{
return isBlack_;
}

public void IsBlack(bool isBlack)
{
isBlack_ = isBlack;
}

// Use this for initialization
void Start () {
}

// Update is called once per frame
void Update () {

}
}
・BoardScript
BoardScriptクラスは、最初にグリッド群(オセロ盤)を生成し、ゲーム中は指定された場所にあるグリッドの情報を(GameScriptクラスに)提供します。
using UnityEngine;
using System.Collections;

public class BoardScript : MonoBehaviour {

public GameObject gridPrefab_, blackStonePrefab_, whiteStonePrefab_;
int colNum_ = 8, rowNum_ = 8; // 縦横のグリッド数
int planeSize_ = 10; // 1グリッドの大きさ
GameObject[] grids_ = new GameObject[64];

public int GetColNum()
{
return colNum_;
}

public int GetRowNum()
{
return rowNum_;
}

public GameObject GetGrid(int colNo, int rowNo)
{
return grids_[rowNo * colNum_ + colNo];
}

// グリッド群(オセロ盤)の生成
void MakeGrids()
{
// 盤中心から端グリッド中心までの距離[グリッド分]
float offsetX = colNum_ / 2 - 0.5f;
float offsetZ = rowNum_ / 2 - 0.5f;

for (int r = 0; r < rowNum_; ++r)
{
float posZ = (offsetZ - r) * planeSize_; // グリッド中心のz座標
for (int c = 0; c < colNum_; ++c)
{
// グリッドの生成
float posX = (c - offsetX) * planeSize_; // グリッド中心のx座標
Vector3 pos = new Vector3(posX, 0, posZ); // グリッド中心の三次元座標
GameObject grid = (GameObject)Instantiate(
gridPrefab_, pos, Quaternion.identity); // グリッドの生成

// グリッドの登録
GridScript gridScript = grid.GetComponent<GridScript>();
gridScript.SetColNo(c);
gridScript.SetRowNo(r);
gridScript.SetBlackStonePrefab(blackStonePrefab_);
gridScript.SetWhiteStonePrefab(whiteStonePrefab_);
gridScript.SetBoardScript(this);
grids_[r * colNum_ + c] = grid;
}
}

// 初期配置の生成
int right = colNum_ / 2;
int left = right - 1;
int bottom = rowNum_ / 2;
int top = rowNum_ / 2 - 1;
grids_[top * colNum_ + left].GetComponent<GridScript>().PutStone(false);
grids_[bottom * colNum_ + right].GetComponent<GridScript>().PutStone(false);
grids_[top * colNum_ + right].GetComponent<GridScript>().PutStone(true);
grids_[bottom * colNum_ + left].GetComponent<GridScript>().PutStone(true);

// Use this for initialization
void Start () {
MakeGrids();
}

// Update is called once per frame
void Update()
{
}
}
・GridScript
GridScriptはグリッドの上に石を置けるか判断する、石を置く、石をひっくり返すといった処理を行います。
石を置けるかの判断や石をひっくり返す処理は再帰処理で実現しています(もっとスマートにできる気が)。
using UnityEngine;
using System.Collections;

public class GridScript : MonoBehaviour {

GameObject stone_;
GameObject blackStonePrefab_, whiteStonePrefab_;
BoardScript boardScript_;
int colNo_, rowNo_;
int colNum_, rowNum_;
int dirNum_ = 8; // 8方向
int[] dirCol_ = new int[8] { -1, 0, 1, -1, 1, -1, 0, 1 }; // 各方向の移動量
int[] dirRow_ = new int[8] { -1, -1, -1, 0, 0, 1, 1, 1 }; // 各方向の移動量

public void SetColNo(int colNo)
{
colNo_ = colNo;
}

public void SetRowNo(int rowNo)
{
rowNo_ = rowNo;
}

public void SetBlackStonePrefab(GameObject blackStonePrefab)
{
blackStonePrefab_ = blackStonePrefab;
}

public void SetWhiteStonePrefab(GameObject whiteStonePrefab)
{
whiteStonePrefab_ = whiteStonePrefab;
}

public GameObject GetStone()
{
return stone_;
}

public void SetBoardScript(BoardScript boardScript)
{
boardScript_ = boardScript;
}

bool SearchSameColorStone(bool isBlackTurn, int dirNum, int colNo, int rowNo)
{
if (colNo >= 0 && colNo < boardScript_.GetColNum() && rowNo >= 0 && rowNo < boardScript_.GetRowNum())
{
GameObject grid = boardScript_.GetGrid(colNo, rowNo);
GameObject stone = grid.GetComponent<GridScript>().GetStone();
if (stone && stone.GetComponent<StoneScript>().IsBlack() == isBlackTurn)
{
return true;
}
else
{
return SearchSameColorStone(isBlackTurn, dirNum, colNo + dirCol_[dirNum], rowNo + dirRow_[dirNum]);
}
}
return false;
}

bool JudgeStonePutableDir(bool isBlackTurn, int dir)
{
// 1個目が自分と異なる色か確認
int colNo = colNo_ + dirCol_[dir];
int rowNo = rowNo_ + dirRow_[dir];
if (colNo >= 0 && colNo < boardScript_.GetColNum() && rowNo >= 0 && rowNo < boardScript_.GetRowNum())
{
GameObject grid = boardScript_.GetGrid(colNo, rowNo);
GameObject stone = grid.GetComponent<GridScript>().GetStone();
if (stone && stone.GetComponent<StoneScript>().IsBlack() != isBlackTurn)
{
// 自分と同じ色の石を探索していく
return SearchSameColorStone(isBlackTurn, dir, colNo + dirCol_[dir], rowNo + dirRow_[dir]);
}

}
return false;
}

public bool JudgeStonePutable(bool isBlackTurn)
{
for (int d = 0; d < dirNum_; ++d)
{
if (JudgeStonePutableDir(isBlackTurn, d))
{
return true;
}
}
return false;
}

void TurnStoneDir(bool isBlackTurn, int dir, int colNo, int rowNo)
{
if (colNo >= 0 && colNo < boardScript_.GetColNum() && rowNo >= 0 && rowNo < boardScript_.GetRowNum())
{
GameObject grid = boardScript_.GetGrid(colNo, rowNo);
GameObject stone = grid.GetComponent<GridScript>().GetStone();
if (stone && stone.GetComponent<StoneScript>().IsBlack() != isBlackTurn)
{
Destroy(stone);
grid.GetComponent<GridScript>().PutStone(isBlackTurn);
TurnStoneDir(isBlackTurn, dir, colNo + dirCol_[dir], rowNo + dirRow_[dir]);
}
else
{
return;
}
}
}

public void TurnStone(bool isBlackTurn)
{
for (int d = 0; d < dirNum_; ++d)
{
if (JudgeStonePutableDir(isBlackTurn, d))
{
TurnStoneDir(isBlackTurn, d, colNo_ + dirCol_[d], rowNo_ + dirRow_[d]);
}
}
}

public void PutStone(bool isBlack)
{
// 黒か白のPrefabを設定
GameObject stonePrefab;
if (isBlack) stonePrefab = blackStonePrefab_;
else stonePrefab = whiteStonePrefab_;

// 石を置く
stone_ = (GameObject)Instantiate(stonePrefab, transform.position, Quaternion.identity);

// 石の色を設定
stone_.GetComponent<StoneScript>().IsBlack(isBlack);
}

// Use this for initialization
void Start()
{
}

// Update is called once per frame
void Update()
{
}
}
・GameScript
GameScriptクラスは、タップを検知し、グリッドがタップされていたら、GridScriptに各処理を実行させます。
using UnityEngine;
using System.Collections;

public class GameScript : MonoBehaviour {

bool isBlackTurn;

// タップを検知し、GridScriptを取得する
bool DetectTap(out GridScript gridScript)
{
gridScript = null;
if (Input.GetMouseButtonDown(0)) // タップ検知
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit = new RaycastHit();
if (Physics.Raycast(ray, out hit))
{
GameObject obj = hit.collider.gameObject; // タップしたGameObjectを取得
gridScript = obj.GetComponent<GridScript>(); // GridScriptを取得
return true;
}
}
return false;
}

// Use this for initialization
void Start () {
isBlackTurn = true;
}

// Update is called once per frame
void Update () {
// タップ検知
GridScript gridScript;
if (DetectTap(out gridScript))
{
// グリッドをタップしており、かつ、そこに石が無いなら
if (gridScript && !gridScript.GetStone())
{
// そこに石を置けるなら
if (gridScript.JudgeStonePutable(isBlackTurn))
{
// 石を置く
gridScript.PutStone(isBlackTurn);

// ひっくり返す
gridScript.TurnStone(isBlackTurn);

// 相手のターンに変更
isBlackTurn = !isBlackTurn;
}
}
}
}
}
あとは、GameScriptとBoardScriptを持つGameObjectを生成したり、各ScriptにPrefabを登録すれば動くはずです。
次回は対戦相手のAIか3D化を実装したいと思います。

続き
http://akasuku.blog.jp/archives/66804617.html

結論から書くと、C#でC++のdllを動かせれば、Unityでも同様に動きます。

・C++dll
extern "C" __declspec(dllexport) float func()
{
    return 1.0f;
}

・C#でC++dllを動かす
using System.Runtime.InteropServices;

namespace dllTestSharp
{
    class Program
    {
        [System.Runtime.InteropServices.DllImport("dllTest.dll")]
        static extern float func();

        static void Main(string[] args)
        {
            Console.WriteLine(func().ToString());
        }
    }
}

・UnityでC++dllを動かす(重力を設定するスクリプトの例)
using System.Runtime.InteropServices;

public class dllTest : MonoBehaviour {

    [System.Runtime.InteropServices.DllImport("dllTest.dll")]
    static extern float func();

    // Use this for initialization
    void Start () {
    }
   
    // Update is called once per frame
    void Update () {
        Physics.gravity = new Vector3(0.0f, func(), 0.0f);
    }
}


MMD4MecanimはUnityでMMDを扱うためのアセットです。
http://stereoarts.jp/

今回はこのMMD4Mecanimを使って、俺妹iP的なものを作ってみました。
画面上のキャラクタに触れると反応を返します。
https://twitter.com/kano_sawa/status/726865536282284032?lang=ja

MMDモデルとモーションは以下のものをお借りしました。
http://www.nicovideo.jp/watch/sm25194175
http://www.nicovideo.jp/watch/sm18000763
http://www.nicovideo.jp/watch/sm21073414

まずは公式のチュートリアルを参考にMMDモデルとモーションをFBXファイルに変換し、Animator Controllerを登録します。
http://stereoarts.jp/MMD4Mecanim%20%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%EF%BC%88%E5%9F%BA%E6%9C%AC%E7%B7%A8%EF%BC%89.pdf

ここで、Animator Controllerは以下のように、触れていない時のモーションが無限ループされるようにしておきます。
Animator

次に、以下のサイトの"モデルのボーンに対してUnityの剛体を追加する"を参考に、Colliderを追加します。
http://qiita.com/mkt_/items/638516e12eec2e5419fd

そして、触れた箇所を判定して個別の反応(モーション)を返すために、Colliderにタグを設定します。
下図では頭部のColliderにHeadタグを設定しています。
タグ

同様に、胸部のColliderにBreastタグを設定します。
タグ2

続いて、ユーザーが触れた部位に応じてアニメーションを切り替えるスクリプトを作成します。
スクリプトは以下のサイトを参考に作成しました。
http://qiita.com/mkt_/items/638516e12eec2e5419fd
using UnityEngine;
using System.Collections;

public class Tap : MonoBehaviour {

float timer = 0.0f; // 元のアニメーションに戻すためのタイマー
const float distance = 1000.0f; // 光線を飛ばす距離
MMD4MecanimModel model = null;

// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {

// 元のアニメーションに戻すためのタイマー
if (timer > 0.0f)
{
timer -= Time.deltaTime;
if (timer < 0.0f)
{
timer = 0.0f;
model.GetComponent<Animator>().CrossFade("立01表情無し.vmd", 0.1f);
}
}

if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;

// 光線がヒットしたら
if (Physics.Raycast(ray, out hit, distance))
{
// MMDのアニメーションを取得する
Collider collider = hit.collider;
model = collider.GetComponentInParent<MMD4MecanimModel>();

// 触れた部位によって設定するvmdファイルを切り替える
switch (collider.tag)
{
case "Head":
SetCrossFade(model, "えへへへへ_.vmd");
break;
case "Breast":
SetCrossFade(model, "ふんっ!_ikari08.vmd");
break;
default:
break;
}
}
}
}

void SetCrossFade(MMD4MecanimModel model, string vmdName)
{
model.GetComponent<Animator>().CrossFade(vmdName, 0.1f);
timer = 3.0f;
}
}
以上で完成です。
以下、補足です。

・補足1
2016/05/06現在のMMD4Mecanim最新版であるBeta_20150821はモバイルに対応していません。
またMMD4Mecanim公式サイトで旧版の配布は行っていません。
スマホなどで動かしたい場合は書籍についてくる旧版などを利用する必要があります。

・補足2
今回利用させていただいたモーションは、0フレーム目に棒立ちのポーズが入っているため、
そのまま使うと、一瞬だけ棒立ちになってからモーションを開始して不自然に見えます。
自然に見せるためには、VMD Editorなどを使って事前にモーションを編集しておく必要があります。

↑このページのトップヘ