2012年12月6日木曜日

必見!Unity初心者が学ぶ「未来事象の正しい文法」


前回記事に引き続いて未来事象の正しい文法についてUnity Gemからの学習を進めていく:


-------
http://unitygems.com/mistakes1/

October 15, 2012

未来事象の正しい文法

未来事象を何か発生させたいだけの場合にコルーチンを用いるのは複雑でやりすぎである。C#で正しく記載することを忘れてしまうことで大混乱に陥ったり、そもそも何の目的で使うか見えなくなりがちだ。Unity初心者の皆さんには未来事象を何か発生させたい場合はコルーチンのことは頭から消し去ってもらい次のアドバイスにしたがってほしい。

タイマーを作成するだけなら以下のコードで良い:

float timer;
int waitingTime;

void Update(){
    timer += Time.deltaTime;
    if(timer > waitingTime){
        //Action
       timer = 0;
    }
}

timer変数は毎フレームごとにdeltaTime(前フレームとの差分の秒数)だけ増分される。この変数は毎フレームwaitingTime(待ち時間)と比較を行う。ついにはtimerがwaitingTimeより大きくなると判定条件はTrueとなりステートメントの中のコードが実行され、最終的にはtimerはゼロクリアされ再度実行できるようになる。timerの増分をinsideというbooleanによる判定条件で囲うことによりtimerが特定の条件で発動されるようにプログラミングすることが出来る。

using UnityEngine;
using System.Collections;
public class Test:MonoBehaviour{
    float timer;    
    int waitingTime;
    bool inside;

    void Start(){
        timer = 0.0;
        waitingTime = 2;
        inside = false;
    }

    void Update(){
        if(inside){
            timer += Time.deltaTime;
            if(timer > waitingTime){
                //Action
               timer = 0;
           }
       }
    }

    void OnTriggerEnter(Collider other){
        if (other.gameObject.tag=="Player")inside = true;
    }
   void OnTriggerExit(Collider other){
        if (other.gameObject.tag =="Player"){
            inside=false;
            timer = 0;
        }
   }
}

上記の例ではプレーヤーがあるゾーンにいる時間で発動するトリガーに関するtimerを作成する方法を示している。例えば火のゾーンに入った場合、プレイヤーが死亡するロジックをUpdateに記述するだけで1秒もたたずにプレイヤーを死亡させることが出来る。このタイマーによりx秒ごとにプレイヤーに対してロジックを働かせることが出来る。

さらにUnityはより優れた解決策も提供しており、次でそれを見ていく。

  • オブジェクトが数秒間だけ生存するスクリプトを書く

もしオブジェクトが画面に数秒だけ表示させたいときは何と、Startの中でロジックを書くだけで良い。Destroyは引数をもたせることで、オブジェクトをDestroyするまでの遅延時間をあらかじめ記述することが出来る。

function Start()
{
       //ゲームオブジェクトを5秒後にDestroy
       Destroy(gameObject, 5);
}

  • アクションの遅延

例えばあるアクションを発生させてから、別のアクションを一定時間経過後に発生させたいとする。UnityScriptとC#で同じ結果のために異なるアクションを用いる。次の例ではcheck変数をif条件に含めることでcheckがtrueになるまでステートメントは実行されない。checkがfalseに設定された場合、2秒後にcheckを再度trueに設定される。

UnityScript

#pragma strict
var check :boolean =true;
var i:int =0;

function Update () {
    if(Input.GetKeyDown(KeyCode.A)&&check){
        check = false;
        print("Inside" + i++);
        WaitForIt();
    }
}
function WaitForIt(){
    yield WaitForSeconds(2.0f);
    check=true;
}

C#

using UnityEngine;
using System.Collections;

public class Wait : MonoBehaviour {
    public bool check =true;
    int i =0;

    void Update () {
        if(Input.GetKeyDown(KeyCode.A)&&check){
            check = false;
            print("Inside" + i++);
            StartCoroutine(WaitForIt());
        }
    }
    IEnumerator WaitForIt(){
        yield return new WaitForSeconds(2.0f);
        check=true;
    }
}
これらのスクリプトを実行することでAボタンを押下することで画面にInsideとiの値を表示しているが、次にまた表示されるまでは2秒遅延される。

  • 何かを数秒後に実行する

もし何かを実行した後、数秒後に別の処理を実行させたい場合、Invokeを使用するのが簡便な方法だ。実行したいスクリプトを含めた関数を記述し、次にInvoke("関数名",遅延秒数)で呼び出す。以下の例ではTurnMeBlue関数は2秒後に実行される。


function Start()
{
      //2秒後にオブジェクトを青にする
      Invoke("TurnMeBlue", 2);
}

function TurnMeBlue()
{
       renderer.material.color = Color.blu10
}

さらに発展させて複数のInvokeを用いることで連続で処理を行わせることができる。


var startPosition : Vector3;
var health = 100.0;
var respawning = false;

function Start()
{
    //オブジェクトの開始位置をキャッシュする
    startPosition = transform.position;
}

//オブジェクトをリセットする
function Respawn()
{
      transform.position = startPosition;
      health = 100;
      renderer.enabled = true;
      respawning = false;
}

//キャラクターを隠す
function Hide()
{
     renderer.enabled = false;
     transform.position = new Vector3(1000,1000,1000);
}

function Update()
{
    if(health < 0 && !respawning)
    {
         respawning = true;
         animation.Play("die");
         Invoke("Hide", 3);
         Invoke("Respawn", 10);
    }
}

このスクリプトはInvokeを使用して死亡アニメーションを再生した後、キャラクターを再生成するものである。最初、healthが0以下になるとdieアニメーションが再生させる。3秒後にモデルは非表示に変更され(位置も変更)、10秒後には再生成する。

  • 数秒おきに何かを実行する

コルーチンを用いたループを記述する以外の方法として、実行したいスクリプトを含めた関数を記述し、次にInvokeRepeating("関数名",初回呼出までの遅延秒数,次回呼出までの遅延秒数)と呼出を行う。

実行はいつでも中断することができ、CancelInvoke("関数名")と引数なしで呼出を行いすべて中断させることが出来る。


float health = 100f;

void Start()
{
    InvokeRepeating("Heal", 2, 2);
}

void Heal()
{
    health = Mathf.Clamp(health + 1, 0, 10011
}

執筆時点では2つめの引数に不具合がある。具体的にはStartと同時に最初の呼出をするために最初の引数をゼロとする。この場合は最初の呼び出しだけは2回実行されてしまう。この不具合解消のためには値をゼロではなく0.01fなどに変更することで回避する。

  • コルーチンを用いるべきケース

コルーチンを用いなければUpdateに記述するしかなく、さらに複雑になりすぎる場合にはコルーチンも有効だといえる。

Updateおよび複数のコルーチンを持たせて同じ変数を更新させる仕様とするのはバグの宝庫であり、不具合が見つけにくくなる。

例えばオブジェクトを異なる角度に回転させたいとする。この場合はコルーチンを用いることが有効である。次の例ではRotateByを呼び出しすることで一定時間の後にオブジェクトを新しい角度に変更する。

function OnMouseUp()
{
     RotateBy(Vector3(0,90,0),2);
}

function RotateTo(angle : Vector3, time : float)
{
     var currentRotation = transform.rotation;
     var targetAngle = currentRotation.eulerAngles + angle;
     var targetRotation = Quaternion.Euler(targetAngle);
     var t = 0;
     while(t < 1)
     {
          transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, t);
          t += Time.deltaTime / time;
          yield null;
     }
     transform.rotation = targetRotation;
}

Javascriptでコルーチンを用いる場合は単にyield記載を加えるのみである。Updateのなかでは正しく動作しないので留意すること。
--------
InvokeRepeatingは用いたことがなかったが、不具合情報とその回避まで整理されているのはUnity Answersの英知ともいえる。

最新の情報を活用して正しいプログラミングをしようぜ!

2 件のコメント:

  1. 始めまして。
    現在、javascriptを用いて、
    ゲーム作りを始めた、PG初心者です。

    ifの条件式を利用して、
    "Jumpflg==true"なら"a"というモーション再生後
    1秒後に"b"を再生したいのですが、
    下記のように"yield"を使用すると上手くいきません。
    どのように書けば良いでしょうか。。。
    あまりに煮詰まってしまったため、
    こちらの記事を見て質問させていただきました。
    宜しくお願いします。

    var jumpobj:GameObject;
    var script:Jump_control;

    function Start () {
    if(!jumpobj){
    jumpobj=GameObject.FindGameObjectWithTag("untagget");
    }
    script=jumpobj.GetComponent("Jump_control");
    }

    function Update () {
    if(script.Jumpflg==true){
    animation.Play("a");
    yield WaitForSeconds(1.0);
    animation.Play("b");
    // }
    }
    else{
    animation.Play("a");
    }
    }

    返信削除
    返信
    1. 返信がおそくなりました!

      初心者の方ならFacebookの「Unityユーザー助け合い所」でご質問いただくと丁寧に教えてくださるかもしれません。

      ところで、書かれているスクリプトについて:

      ・Update()は1秒間の間に複数回呼び出しされる
       →Jumpflgがfalseのとき、animation.Play("a")が何回も始めから再生される
       →Jumpflgがtrueのとき、animation.Play("b")が何回も始めから再生される
      ということですから、アニメーションが始めから再生されるばかりでうまく行かないことが原因にみえます。

      このブログのサンプルでいうと、

      if(health < 0 && !respawning)
      {
      respawning = true;
      animation.Play("die");
      Invoke("Hide", 3);
      Invoke("Respawn", 10);
      }

      における、respawning変数のように、アニメーションが何回も始めから再生されないように試したほうが良いようにおもいます。

      すでに解決済みであったらすみませんが、いろいろと自分で試した後にUnityユーザ助け合い所に聞いてみたりして、最後までゲームとして完成できるように頑張ってみてください!

      削除

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿

ブログ アーカイブ