• 検索結果がありません。

No Programming No Creating game ~ とある似非プログラマーの死 ~

ドキュメント内 NPCA部誌2018 (ページ 42-49)

第 4 章 簡単 !VR3(00) 分クッキング 34

4.7 No Programming No Creating game ~ とある似非プログラマーの死 ~

この問題の解決には少し時間がかかりました。Blender側でマテリアルをいじっても全然反映されないのです。考えてみるとこ れは当たり前で、Blenderで設定しているのはあくまでBlenderでレンダリングする際の設定なので、この設定がUnityに反映さ れる訳がないのです。という訳でUnity側でマテリアルを編集しましょう。

めでたく皿が光るようになりました。やれやれ、やっとできた...ダメでした。ちょっと前に書いたBlenderのエクスポート設 定を知らなかった自分は普通に皿を回転させて使っていたのです。ここに大きな罠が潜んでいて、Unityにおいて回転されたオブ ジェクトのx,y,z軸は回転する前と角度が変わってしまうのです。モデルをもう一度エクスポートし直せばいいのですが、そうと 知らなかった当時の僕は皿のモデルの親にEmptyを作り、Emptyを回転させることで解決しました。この問題はこのあと腕を実 装するときにも再び発生します。何はともあれひとまず問題は解決しました。

あとは適当に壁や天井を作りましょう。ここは本質ではないので手抜きです。

天井を設置すると太陽光(最初からあるDirectional Light,平行光線)が通らなくなり画面が暗くなります。そこで天井の中心付 近にPoint Lightを1個設置しました。

これで一通り配置できました。お次はプログラミングです。一番面倒、というかこれが本編かもしれません。

4.7 No Programming No Creating game ~ とある似非プログラマーの死 ~

おい、このゲーム動かねぇぞ! 当たり前だろ、プログラム1個も書いてないんだぞ! はい。プログラムのないゲームが動くはず がないのでプログラミングしていきましょう。ちなみに言語はC#です。

まずはプレイヤーの移動スクリプトです。回転寿司ですのでレーンの上をレーンに沿って動く必要があります。ここからはスク リプトを貼り付けて解説するスタイルで行きます。ちなみにファイル名は適当です。というわけで移動スクリプト...の前に一つ。

第4章 簡単!VR3(00)分クッキング 4.7 No Programming No Creating game ~とある似非プログラマーの死~

都合上スクリプトの名前をゲーム内での名前と変えている場合があります。本来スクリプトの名前とclassの後に書くものは同じ でなければなりません。

移動スクリプト

Move.cs

usingSystem.Collections;

usingSystem.Collections.Generic;

usingUnityEngine;

public classmoveOther: MonoBehaviour { Vector3 center;

Vector3 center2;

float starttime;

// Use this for initialization voidStart() {

center= GameObject.Find("center").transform.position;

center2=center;

center2.x= -center.x;

starttime=Time.time;

}

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

if (transform.position.x> 4f) {

transform.RotateAround(center2,Vector3.up, -180f / 14f *Time.deltaTime);

}else if(transform.position.x< -4f) {

transform.RotateAround(center,Vector3.up, -180f/ 14f*Time.deltaTime);

}

else if(transform.position.z< 0) {

transform.position += newVector3(0.5f *Time.deltaTime, 0, 0);

} else{

transform.position += newVector3(-0.5f* Time.deltaTime, 0, 0);

} } }

もっと短くなるかもしれませんが、これをもとに説明していきます。

最初の3行はおまじないだと思ってて下さい。"名前空間"でググると大体わかると思います。

次のpublic~もUnityでスクリプトを作ると最初から書いてある部分で、変更してはなりません。

実際の処理は次の行からです。最初の数行では処理に使う変数を定義しています。変数とは色々な型の値を取り扱うもので、座 標を表す変数や整数を表す変数など様々なものがあります。ここでは移動する為に必要な座標を表す変数と時間を記録する変数を 定義しています。

’//’はコメントアウトです。//をかいた行は無視されるのでコメントを書くことができます。

次のVoid Start()でStart関数を定義しています。Startのあとの’{’から、数行下の’}’までがStart関数です。関数とい うのは、足し算引き算等の様々な処理をまとめて記述したもので、関数を呼び出すだけで、関数内で書かれた処理がすべてまとめ て行われます。Start関数というのはUnityがスクリプトを実行するときに、最初に1回だけ実行される関数で、ここで値の代入 やオブジェクトの初期配置等の処理を行います。今回のスクリプトでは皿を動かすときの目印となる地点の座標と、タイマーの最 初の時間を決めています。

次のVoid Update()でUpdate関数を定義しています。Update関数はUnityが毎フレーム実行する関数です。フレームとい うのはすべての処理を始めてから終わるまでの時間で、1秒間に数十から数百回フレームが加算されます。フレーム数はプログラ ムの重さによって変わります。重いゲームでは、処理に時間がかかるので、必然的にフレーム数は落ちます。秒間フレーム数は常 に変化するので、移動用の関数などをUpdate関数内で使う場合には後述する処理が必要です。

ではUpdate()関数内の移動処理を解説していきます。はじめのifは条件分岐です。()内の条件を満たしていると{}内の処理が 行われます。その次のelse内に書かれた処理は直前のif文の条件を満たしていない場合に実行されます。else ifは直前のif文の条 件を満たしていない、かつ()内の条件を満たしているときに実行されます。詳しく見ていきましょう。

3 つの()内で皿が今どこにいるかを判断しています。直線部なのか円周部なのか、みたいな感じです。直線部なら trans-form.position(皿の場所)のX座標を1秒に0.5ずつ増やし、円周部ではtransform.RotateAround()関数でcenterという座標 を中心に毎秒180/14°回しています。Vector3というのは3次元ベクトルを表す型で、Time.deltaTimeは先程書いたUpdate 関数の問題点を解決する為に書いています。Time.deltaTimeでそのフレームを処理するのにかかった時間(秒)を取得すること

第4章 簡単!VR3(00)分クッキング 4.7 No Programming No Creating game ~とある似非プログラマーの死~

粋して載せていこうと思います。

このゲームでは腕が飛んでくる仕様なのでお次は腕を生成するスクリプトと生成した腕を動かすスクリプト。その前に腕の説明 をしておくと、現状Unityの標準の3DモデルであるCylinderを利用しています。文化祭本番では腕のモデルになってる...かも しれません。まずは生成から

腕を生成

eatSushi.cs

public classeatsushi :MonoBehaviour { Transform arm_point;

Transform dish;

public GameObject arm_pf;

float time;

// Use this for initialization voidStart() {

arm_point=transform.Find("arm");

dish=GameObject.Find("Main Camera").transform;

Vector3 add= arm_point.transform.position;

add.x =add.x +Random.Range(-0.5f, 0.5f) - 0.5f;

add.y += Random.Range(-0.5f, 0.5f);

arm_point.transform.position =add;

arm_point.transform.LookAt(dish);

time= 0;

Instantiate(arm_pf,arm_point.transform.position,arm_point.transform.rotation);

}

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

time+= Time.deltaTime;

if (time > 5f) {

Instantiate(arm_pf,arm_point.transform.position, arm_point.transform.rotation);

time = 0;

} } }

ちょっと長いですね...では解説していきます。

まずこのスクリプトは捕食者に対してアタッチします。

はじめのTransformはオブジェクトの位置や角度などをまとめて記憶する型です。座標と角度を指定して使う関数の引数に指

定できます。GameObjectは、1つのオブジェクトの持つ情報をすべて格納できる型です。

Start関数内では生成すべき腕を読み込み、所定の座標に乱数を足した座標を生成し、Instantiate()で生成しています。ま た、一定時間ごとに生成する為にタイマーを設定しています。

Update関数内で5秒ごとに生成する処理を記述しています。Start関数内で処理を開始した時間を記録しておき、Time.time で取得した今の時間との差が5を超えたときにタイマーを0にセットし、腕を生成すればいいわけです。

お次は腕を動かすスクリプト。命名が適当だけど許ちて。

腕を飛ばす

Shot.cs

public classmomove: MonoBehaviour { Transform dish;

// Use this for initialization float starttime;

int attack;

voidStart () {

dish=GameObject.Find("TrackingSpace/CenterEyeAnchor").transform;

//transform.Rotate(new Vector3(1, 0, 0) * 180);

transform.LookAt(dish);

starttime=Time.time;

attack= Random.Range(0, 4);

}

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

if (attack == 0) normalAttack();

else if(attack== 1) fastAttack();

elseballisticAttack();

}

voidnormalAttack() { transform.LookAt(dish);

transform.Rotate(newVector3(1, 0, 0) * 90);

第4章 簡単!VR3(00)分クッキング 4.7 No Programming No Creating game ~とある似非プログラマーの死~

transform.Translate(Vector3.up *Time.deltaTime * 0.75f);

}

voidfastAttack() { transform.LookAt(dish);

transform.Rotate(newVector3(1, 0, 0) * 90);

transform.Translate(Vector3.up *Time.deltaTime * 1f);

}

voidballisticAttackfh() {

transform.Translate(transform.up *Time.deltaTime* 1.5f);

}

voidballisticAttacklh() { transform.LookAt(dish);

transform.Rotate(newVector3(1, 0, 0) * 90);

transform.Translate(Vector3.up *Time.deltaTime * 2f);

}

voidballisticAttack() {

if (Time.time-starttime < 3f) { ballisticAttackfh();

} else{

ballisticAttacklh();

} } }

長い...。この関数は腕自体にアタッチします。生成するスクリプトから制御しようと思ったのですが、複数生成されたオブジェ クトのそれぞれに個別の処理をすることができなかったので、腕自体にアタッチしました。結果的にこのほうがコードが書きやす くなってると思います。

腕には3種類の飛ばし方を設定しました。Start関数内でランダムに切り替えています。またTransform型のdishにVRの 目を指定し、そこをめがけて腕が飛ぶようにしています。この仕様によって目の前に腕が迫ってくる演出ができます。

3種類それぞれの説明をする前に1つだけ。各関数のどれも、移動する前にRotateしていたり、移動の方向もオブジェクトに対 する前方ではなく上方になっています。ここには大きな罠(?)が潜んでいて、Unityの円柱はもともと前方が側面になっているの です(多分)。腕の親にEmptyを設定して、このEmptyを回せば解決する気がするのですが、実験している時間がなかったのでこ のような形になっています。

第4章 簡単!VR3(00)分クッキング 4.7 No Programming No Creating game ~とある似非プログラマーの死~

では3種類を見ていきましょう。

normalAttackはnormalなAttackです。最もゆっくり飛んできます。最初にdish(目の中心)の方向を向き、少し上に書いた 処理を行い、上方向(要するに皿の方向)めがけて秒速0.75で飛ばしています。

fastAttackはfastなAttackです。速いです。処理方法はnormalAttackと同じですが、速度が秒速1になっています。

ballisticAttackはballisticAttackfhとballisticAttacklhから出来ています。まず上に飛んで行って、そこから皿 めがけて飛んでくる仕様です。飛び始めてからの時間3秒以下の時ballisticAttackfhが実行され、腕は秒速1.5で上へ飛んで いき、その後ballisticAttacklhが実行され下向きに秒速2で飛んできます。

どれも飛ばし方自体は大したことありませんね。

では次。パンチ&当たり判定です。多分ここが一番大変でした。飛んできた腕をパンチする部分と、消しきれなかった腕が顔に 当たる処理です。2つとも貼ります。

パンチ & 当たり判定

hit.cs

usingSystem.Collections;

usingSystem.Collections.Generic;

usingUnityEngine;

usingUnityEngine.VR;

public classhit :MonoBehaviour { float vl;

Vector3 dif;

Vector3 dif1;

Rigidbody rb;

AudioClip sound;

die Die;

int score = 0;

// Use this for initialization voidStart () {

rb =this.GetComponent<Rigidbody> ();

dif= InputTracking.GetLocalPosition(VRNode.RightHand);

sound =GetComponent<AudioSource>().clip;

Die= GameObject.Find("CenterEyeAnchor").GetComponent<die>();

}

voidOnTriggerEnter(Collider other) { if (other.gameObject.tag == "enemy") {

Destroy(other.gameObject);

if (vl > 2f) { Ddie();

GetComponent<AudioSource>().PlayOneShot(sound);

} } }

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

dif1=InputTracking.GetLocalPosition(VRNode.RightHand) -dif;

vl =Vector3.Magnitude(dif1);

vl =vl /Time.deltaTime;

dif= InputTracking.GetLocalPosition(VRNode.RightHand);

}

voidDdie() { Die.combo++;

Die.score += Die.scorep;

Die.hit = 0;

} }

第4章 簡単!VR3(00)分クッキング 4.7 No Programming No Creating game ~とある似非プログラマーの死~

die.cs

public classdie :MonoBehaviour { public inthit = 0;

public intcombo = 0;

public intscorep = 2000;

public intscore = 0;

// Use this for initialization voidStart () {

}

voidOnTriggerEnter(Collider other) { if (other.gameObject.tag == "enemy") {

hit++;

Destroy(other.gameObject);

} }

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

if (hit >= 6) { hit = 0;

combo = 0;

}

if (combo >= 5) { scorep += 10000;

} } }

上が (右) 腕と用スクリプトで下がプレイヤーへの当たり判定用のスクリプトです。右腕と書いたのは、スクリプト中で

RightHandと書いてある部分を左腕ではLeftHandにする必要があるからです。VR機器のデータを取り扱う場合には上のスク

リプトのようにusing UnityEngine.VRを記述する必要があります。上のスクリプトで音を鳴らす処理をしている箇所があり ます。音はAsset Storeなどで探しましょう。さすがに自作する気力はありませんでした。音が用意出来たら右腕(OvrAvatarを 使っている場合)Tracking Space直下のRightHandAnchorにアタッチします。

パンチ

先に上のスクリプトの処理の中身を書きます。上のスクリプトではパンチに関する判定を行っています。具体的には自分の腕が 飛んできた腕に一定の速度以上でぶつかったときに飛んできた腕を消すという処理です。では説明していきます。

初めの変数の宣言でRigidbodyとdieという新しい型が登場しています。RigidbodyはUnityが物理演算の為に使う項目 です。die型の変数を宣言すると、その変数がdie.cs(下のスクリプト)に書いてある内容を取得する為に使えるようになります。

Start関数内でdie型のDieに、CenterEyeAnchor(目の中心)にアタッチされたdie.csを代入しています。あるオブジェクトに 付いている要素を取得するには、対象のオブジェクト.GetComponent<取得したい要素>()を実行すれば良いです。対象のオブ ジェクトは、対象がアタッチ先のGameObjectの場合は書かなくてもよいですが(わかりやすくする為にthisと書いてもよい)、そ うでない場合はGameObject.Find("対象の名前")で対象のGameObjectを発見できます。またdifに右腕の座標(現実世界にお ける)を代入しています。

OnTriggerEnter関数は物体が他の物体に当たった時に呼び出される関数です。関数の中身は自分で記述します。この関数を利

用する為には衝突する物体と衝突される物体の双方にColliderとRigidbodyコンポーネントがアタッチされている必要がありま

す。()内のotherは関数の引数です。ここに衝突した相手の情報が入って関数が呼び出されます。衝突した相手のtag(詳細はグ

グって)がenemyの時に衝突相手を消します。この場合だと飛んできた腕が消えます。また次のif文は衝突時のプレイヤーのパン

チの速さによって分岐します。パンチが一定速度以上の時に音が鳴り、Ddie関数が呼ばれます。Ddie関数は下のdie.csで定義し たcomboを増やす処理とスコアを加算する処理とhitを帳消しにする処理を記述しました。

パンチの速度の検知が少し大変でした。試行錯誤の結果、現在の拳の座標(dif1)と1フレーム前の拳の座標(dif)の3次元ベク トルの差を取得してベクトルの長さを計算し、Time.deltaTimeで割っています。なぜTime.deltaTimeで割るかですが、理由 は簡単で、速さ=距離/ 時間だからです。ベクトルの長さはVector3.Magnitude()で求められます。この関数は与えられた座 標のsprt(x*x + y*y + z*z)を返します。

ドキュメント内 NPCA部誌2018 (ページ 42-49)