B-Teck!

お仕事からゲームまで幅広く

【C#/Unity】相手を追尾するAIを考える その2

前回 beatdjam.hatenablog.com

はじめに

さっきUnityをインストールして環境作ったので、昨日の記事を改めてUnityで実装してみました。
今回も最後に全文のソースコードを載せます。

また、今回掲載するソースは以前掲載した下記記事から一部流用しています。
こちらの記事もよろしくお願いします。 beatdjam.hatenablog.com

前回の記事の内容に合わせて簡単に解説します。

解説

動作のgif
f:id:beatdjam:20160628202903g:plain

changeDirection

前回の記事で実装したmoveEnemychangeDirectionに対応します。
基本的な考えは変わらず、目標と自分自身の位置の差分を取得し、縮めるように動かすだけです。
ポイントはtransform.rotationQuaternion.Eulerですかね。
transform.rotationは、オブジェクトから見た相対的な回転ではなく、シーン全体から見た回転量を表します。
インスペクタに出てくるやつですね。
Quaternion.Eulerは、角度をtransform.rotationに対応した値に変換するメソッドです。
Y軸のみを変えることで正面を常に移動したい方向に向けています。

//playerの座標に応じて向きを変える
void changeDirection(GameObject player){
    //playerとenemyの座標差分を取得する
    int xDistance = Mathf.RoundToInt(this.transform.position.x - player.transform.position.x);
    int zDistance = Mathf.RoundToInt(this.transform.position.z - player.transform.position.z);

    //向きたい角度
    int rotateDir = 0;

    //x座標,z座標の差分から向きたい角度を取得する
    //playerとenemyに距離がある場合
    if(xDistance == 0){
        //x座標が同じ場合z座標のみ向き取得
        rotateDir = this.getDirection(zDistance, "z");
    }else if(zDistance == 0){
        //z座標が同じ場合x座標のみ向き取得
        rotateDir = this.getDirection(xDistance, "x");
    }else{
        //どちらも差がある場合、ランダムで進む向き取得
        int rand = UnityEngine.Random.Range (0, 2);
        if(rand == 0){ 
            //z座標向き取得
            rotateDir = this.getDirection(zDistance, "z");
        }else{
            //x座標向き取得
            rotateDir = this.getDirection(xDistance, "x");
        }
    }

    //取得した方向にオブジェクトの向きを変える
    this.transform.rotation = Quaternion.Euler (0, rotateDir, 0);
}

getDirection

前回の記事で実装したgetMovePosgetDirectionに対応します。
こちらは少し見栄えが変わったかなと思います。
渡された軸、距離から、どちらの軸を移動するか、軸に対してマイナス・プラスのどちらに移動するかを算出しています。
0 90 180 270 がそれぞれ4方向に対応しています。

//向きの角度を取得する
int getDirection(int distance,string axis){
    //距離がプラスかマイナスかを取得
    int flag = distance > 0 ? 1 : 0;

    //角度を返却
    if(axis == "x"){
        return flag == 1 ? 270 : 90;
    }else{
        return flag == 1 ? 180 : 0 ;
    }
}

おわりに

わからないことがあればまずはググるかUnityのリファレンスを読んでみましょう。
できることや方法、考え方を知ることは、間違いなくプログラマーとしての力につながります。
自分なりに考えてみて、それでもわからなければ、コメント欄等で質問いただければ力になります!

というわけで、次回(があれば)は経路探索についてかけたらいいなと思います。

ソースコードは下記になります。

using UnityEngine;
using System.Collections;

public class nearTest : MonoBehaviour{    
    private GameObject nearObj;      //最も近いオブジェクト
    private float searchTime = 0;    //経過時間

    void Start(){
        //最も近かったオブジェクトを取得
        this.nearObj = this.serchTag(gameObject, "Player");
    }

    void Update () {
        //経過時間を取得
        this.searchTime += Time.deltaTime;

        if (this.searchTime >= 1.0f) {
            //最も近かったオブジェクトを取得
            this.nearObj = this.serchTag(gameObject, "Player");
            //経過時間を初期化
            this.searchTime = 0;

            //1秒おきに移動
            //対象の方向を向く
            this.changeDirection(this.nearObj);
            //自分自身の位置から相対的に移動する
            this.transform.Translate(Vector3.forward * 1.00f);
        }   

    }

    //playerの座標に応じて向きを変える
    void changeDirection(GameObject player){
        //playerとenemyの座標差分を取得する
        int xDistance = Mathf.RoundToInt(this.transform.position.x - player.transform.position.x);
        int zDistance = Mathf.RoundToInt(this.transform.position.z - player.transform.position.z);

        //向きたい角度
        int rotateDir = 0;

        //x座標,z座標の差分から向きたい角度を取得する
        //playerとenemyに距離がある場合
        if(xDistance == 0){
            //x座標が同じ場合z座標のみ向き取得
            rotateDir = this.getDirection(zDistance, "z");
        }else if(zDistance == 0){
            //z座標が同じ場合x座標のみ向き取得
            rotateDir = this.getDirection(xDistance, "x");
        }else{
            //どちらも差がある場合、ランダムで進む向き取得
            int rand = UnityEngine.Random.Range (0, 2);
            if(rand == 0){ 
                //z座標向き取得
                rotateDir = this.getDirection(zDistance, "z");
            }else{
                //x座標向き取得
                rotateDir = this.getDirection(xDistance, "x");
            }
        }

        //取得した方向にオブジェクトの向きを変える
        this.transform.rotation = Quaternion.Euler (0, rotateDir, 0);
    }

    //向きの角度を取得する
    int getDirection(int distance,string axis){
        //距離がプラスかマイナスかを取得
        int flag = distance > 0 ? 1 : 0;

        //角度を返却
        if(axis == "x"){
            return flag == 1 ? 270 : 90;
        }else{
            return flag == 1 ? 180 : 0 ;
        }
    }

    //指定されたタグの中で最も近いものを取得
    GameObject serchTag(GameObject nowObj,string tagName){
        float tmpDis = 0;           //距離用一時変数
        float nearDis = 0;          //最も近いオブジェクトの距離
        GameObject targetObj = null; //オブジェクト

        //タグ指定されたオブジェクトを配列で取得する
        foreach (GameObject obs in  GameObject.FindGameObjectsWithTag(tagName)){
            //自身と取得したオブジェクトの距離を取得
            tmpDis = Vector3.Distance(obs.transform.position, nowObj.transform.position);

            //オブジェクトの距離が近いか、距離0であればオブジェクト名を取得
            //一時変数に距離を格納
            if (nearDis == 0 || nearDis > tmpDis){
                nearDis = tmpDis;
                targetObj = obs;
            }
        }
        //最も近かったオブジェクトを返す
        return targetObj;
    }
}

【JavaScript】相手を追尾するAIを考える その1

はじめに

この記事にこんなコメントがつきました。
beatdjam.hatenablog.com

コメント失礼します。 ゲーム開発を学んでいるプログラマーの卵です。

敵AIの追尾について勉強していて、 縦横マス目移動でプレイヤーを追尾出来るような敵の処理にしたいのですが、中々上手くいっていない状態です。

どのようにしたら縦横マス目移動で追尾してくれますでしょうか。 よろしくお願いします。

今回は状況がわからないため、一番簡単な障害物のないXY平面上でPlayerを追尾するEnemyという想定で、
どのように考えていけばいいかを説明していきたいと思います。

追尾の考え方

追尾というのは難しく考えてしまいがちですが、実質「現在の状態から相手に1マス近づく」という行動の繰り返しです。

f:id:beatdjam:20160627202855p:plain

1マスずつ近づくために必要な状況を切り分けて見ると、どう実装するべきかがわかってきます。

問題の切り分け

さて、実際どう実装するかを考えていきましょう。
X座標、Y座標を近づけたい場合は下記の3つの状況が考えられると思います。
f:id:beatdjam:20160627205039p:plain
X座標・Y座標両方があっていない場合は、今回はランダムでどちらかを選択するようにしてみましょう。

実装

今回はJavaScriptで実装してみました。動かすのが楽だったので。
moveEnemyとgetMovePosが今回の解説部分です。
コンソールに、Playerに近づいていく様子が出力されます。

おわりに

ざっくりとした説明でしたが、雰囲気だけでも掴んでいただけたでしょうか。
しかし、今回の実装ではマップ上に障害物や進めないエリアがあった場合に簡単に動けなくなってしまいます。
この問題を解決するには、経路探索と呼ばれるロジックを実装する必要があるのですが…、少し長くなってしまうので次回以降に記事にできたらいいなと思います。
基本の考え方として、「相手の位置を確定する」、「自分がどう移動するかを考える」という点は変わらないので、参考になれば幸いです。

最後に一応こちらにもソースを置いておきます。

"use strict"
process.stdin.resume();
process.stdin.setEncoding('utf8');

/*
* 設定値エリア(色々変えてみてください)
*/
//player、enemyそれぞれにx,y座標を設定する
var player = {"x":1,"y":1};
var enemy = {"x":4,"y":3};

//移動回数
var count = 6;

//マップの広さ
var xmax = 4;
var ymax = 4;

/*
* 実行
*/
//enemyを移動
for(var i = 0;i < count;i++){
  //現在のマップ出力
  renderingMap(player,enemy,xmax,ymax);  
  //enemyを動かす
  enemy = moveEnemy(player,enemy);
}

/*
* 関数定義
*/
//playerの座標に応じてenemyを移動させる
function moveEnemy(player,enemy){
  //playerとenemyの距離差分を取得する
  var xDistance = enemy.x - player.x;
  var yDistance = enemy.y - player.y;
  
  //playerとenemyに距離がある場合
  if(xDistance === 0){
    //x座標が同じ場合y座標のみ移動
    enemy.y = getMovePos(yDistance, enemy.y);
  }else if(yDistance === 0){
    //y座標が同じ場合x座標のみ移動
    enemy.x = getMovePos(xDistance, enemy.x);
  }else{
    //どちらも差がある場合、ランダムでどちらか1座標移動する
    var rand = Math.floor( Math.random() * 2);
    if(rand === 0){
      enemy.y = getMovePos(yDistance, enemy.y);      
    }else{
      enemy.x = getMovePos(xDistance, enemy.x);        
    }
  }
  
  return enemy;
}

//playerの位置関係によって移動方向を分け、移動先を返す
function getMovePos(distance, pos){
    if(distance > 0){
      pos--;
    }else{
      pos++;        
    }     
    return pos;  
}

//map描画
function renderingMap(player, enemy,xmax,ymax){
    for(var j = 1;j <= ymax;j++){
        var tmpStr = "";
        for(var k = 1;k <= xmax;k++){
            if(player.x === k && player.y === j){
                tmpStr += "p";
            }else if(enemy.x === k && enemy.y === j){
                tmpStr += "e";
            }else{
                tmpStr += "*";
            }
        }
        console.log(tmpStr);  
    }
    console.log(" ");
}

【PHP】標準入出力を行うクラス

<?php
    class inputManager{
        private $index = 0;
        private $input = [];
        //区切り文字設定
        const SPRIT_STR = " ";
        
        /**
         * コンストラクタ
         * 
         * @access public
         * @param  なし
         * @return なし
         */
        public function __construct($testFlg = false, $testInput = null){
            if(!$testFlg){
                    $input_lines = trim(fgets(STDIN));
                    $input_lines = str_replace(array("\r\n","\r","\n"), "", $input_lines);
                    do {
                        $this->input[] = $input_lines;
                        $input_lines = trim(fgets(STDIN));
                        $input_lines = str_replace(array("\r\n","\r","\n"), "", $input_lines);
                    } while ($input_lines !== "");
            }else{
                $this->input = $testInput;
            }
        }
        
        /**
         * value
         * 現在のインデックスの値を取得する
         * 
         * @access public
         * @param  なし
         * @return string|null 現在のインデックスの値
         */
        public function value(){
            if($this->index < $this->length()){
                return $this->input[$this->index];
            }else{
                return null;
            }
        }
        
        /**
         * explodeValue
         * 現在のインデックスの値をexplodeした配列を取得する
         * 
         * @access public
         * @param  なし
         * @return Array|null explode後配列
         */
        public function explodeValue(){
            if($this->index < $this->length()){
                return explode(self::SPRIT_STR, $this->input[$this->index]);
            }else{
                return null;
            }
        }
        
        /**
         * setindex
         * インデックスを設定する
         * 
         * @access public
         * @param  $i_index 設定するインデックス
         * @return $this
         */
        public function setindex(int $i_index){
            $this->index = $i_index;
            return $this;
        }
        
        /**
         * next
         * インデックスを1つすすめる
         * 
         * @access public
         * @param  なし
         * @return $this
         */
        public function next(){
            $this->index++;
            return $this;
        }
        
        /**
         * length
         * 入力の長さを取得
         * 
         * @access public
         * @param  なし
         * @return int $this->input 配列の長さ
         */
        public function length(){
            return count($this->input);
        }
    }
    
    class out{
        /**
         * output
         * 引数を出力
         * 入力がない場合は空行を出力する
         * 
         * @access public
         * @param  string $i_val 出力する文字列
         * @return なし
         */
        public static function output(string $i_val = ""){
            echo $i_val. "\n";
        }
    }
    
    //テスト用入力
    $input[] = "123";
    $input[] = "456";
    $input[] = "789";
    $input[] = "aaa";
    $input[] = "bbb";
    
    $io = new inputManager(true,$input);
    //長さの取得
    out::output($io->length());
    
    //
    out::output();
    
    //インデックス設定の確認
    out::output($io->value());
    $io->next();
    out::output($io->value());    
    $io->setindex(2);
    out::output($io->value());    
    out::output($io->next()->value());   
    out::output($io->next()->value());   
    
    out::output();
    
    //入力を末尾まで出力する
    $io->setindex(0);
    while(!is_null($io->value())){
        out::output($io->value());
        $io->next();
    }
    
    out::output();
    
    //インデックスをセットして末尾まで出力する
    for($i = 0;$i < $io->length(); $i++){
        out::output($io->setindex($i)->value());
    }