この記事の役割
この記事はチャリ走のような無限に続く横スクロールゲームで、ステージを無限に、ランダムに生成していく方法を書いていきます。
Unityをインストールした直後の初心者でも操作が分かるように書いていくつもりです。知ってるところは飛ばしていって下さい。
##環境
- Unity2019.3.0
- Windows10
僕自身よくわかっていないところもあるので、間違えているところがあったらコメント等で教えていただけると幸いです。
##ステージの無限生成とは
今回は、以下の図のように無限にステージが出来るものを指しています。チャリは遊び心で置いてみただけで、本記事では扱いません。
##手順1 prefabを作る
prefab(プレハブ)は、同じようなオブジェクトを複数個生成したり、同じような動きをさせたりするのに使います。
今回の場合は、ステージの一つ一つがCubeで作られていて、それを大量に複製してできています。
今回の場合、足場を繰り返し大量に生成することになるのでprefabが適しています。
では、実際にprefabを作っていきましょう。まず、画像の手順でCubeを生成してみてください。
次に、InspectorビューからCubeにPlaneと名前を付け、大きさを(4.5,0.5,1)とします。
この大きさに深い意味があるわけではないのですが、このくらいの大きさにしておくとチャリ走の難易度がちょうど良いです。
次に、これをprefabにします。
HierarchyビューからProjectビューにPlaneをドラッグします。こうするとPlaneの文字が青くなったはずです。
この状態になったのを確認したら、Hierarchyビューの方のPlaneは不要なのでdeleteで消しましょう。
これでprefabを作ることができました。
##手順2 スクリプトを作る
Projectビューの「+」マークからC#スクリプトを選択し、「PlaneScript」と名前を付けましょう。
次に、PlaneScriptをダブルクリックして、スクリプトを編集します。
##手順3 Planeを生成する
スクリプトからPlaneを生成してきましょう。
今回はprefabが1種類しかないので、publicをつけてInspectorビューから指定していきたいと思います。
prefabを使う際はResouces.Load()を使う場合も多いです。その場合の方法はこちらの記事に分かりやすくまとまっています。
https://qiita.com/2dgames_jp/items/8a28fd9cf625681faf87
今回は違うやり方をやるので、以下のスクリプトをPlaneScriptにコピペしてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlaneScript : MonoBehaviour
//ここまではおまじないみたいなものだと思ってくれればOK。
{
public GameObject Plane;
//ここでpublicと宣言することで後でInspectorビューから操作できる
float timer = 0;
float spowntime = 2; //2秒ごとに生成させる
void Update()
{
timer += Time.deltaTime; //timerの値を1秒に1のペースで増やす
if(timer > spowntime) {
PlaneGenerate(); //PlaneGenerate関数を呼び出す。
timer = 0; //timerを0に戻す。
}
}
void PlaneGenerate() {
Instantiate(Plane, new Vector3(1, 0, 0),Quaternion.identity);
/*Planeを(1,0,0)の場所に生成する。
Quaternion.identityは回転させないことを示す言葉*/
}
}
//とか、/*とかはコメントといってスクリプトに直接関係しません。
さて、このスクリプトを保存したらUnityに戻ってください。
スクリプトは単体だと働かないので、何かにくっつけなくてはいけません。今回はCreatEmptyでできた空のGameObjectにスクリプトをくっつけていきます。
下図に沿ってやってみてください。
次に、Inspectorビューからpublic関数で宣言したPlaneを下図に沿って指定してください。
Publicをつけて宣言したGameObjectは必ずInspectorビューから指定することを忘れないようにしましょう。筆者はよく忘れます。
ここまで出来ればPlaneの生成はOKです。
##手順4 Planeを動かす
上のチャリ走のGIF画像をもう一度見て下さい。これ、チャリは横方向に動いていないの分かりますか?
実は、チャリが横方向に動く代わりに、全てのPlaneを右から左に動かしています。
ということで、これをスクリプトに書いていきましょう。
新しくスクリプトを生成し、名前をPlaneMoveScriptとして、以下のスクリプトをコピペしてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlaneMoveScript : MonoBehaviour
{
float speed = 5;
void Update()
{
this.gameObject.transform.position -= new Vector3(speed * Time.deltaTime, 0, 0);
}
}
this.gameObject.transform.positionというのは、このスクリプトがくっついているGameObjectの座標という意味です。このスクリプト全体で、xの負方向に毎秒5ずつ移動させなさいという命令です。
ここまで出来たらUnityに戻って、このスクリプトをProjectビューにあるPlaneのprefabにドラッグしてください。
それでは、実行してみましょう。
このように、生成されたPlaneが移動していけば成功です。
##手順5 無限に生成する①一定時間後にDestroy
手順4までで無限にオブジェクトを生成することは出来るようになりました。
しかし、このままではゲームをやっていくにしたがって、オブジェクトの数が多くなり、どんどん処理が重くなってしまいます。
そこで、一定時間後にPlaneを破壊するように設定しましょう。
PlaneMoveScriptを開いてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlaneMoveScript : MonoBehaviour
{
float speed = 5;
//以下を追加
void Start()
{
Destroy(this.gameObject, 10);
}
//ここまで
void Update()
{
this.gameObject.transform.position -= new Vector3(speed * Time.deltaTime, 0, 0);
}
}
Start関数の中にDestroy(this.gameObject, 10)
と書いてください。
このスクリプトがくっついているGameObjectを10秒後に破壊する、という意味です。
では、実行してみましょう。
図のように、Plane(Clone)の個数が5個で止まっていたら成功です。
これでマップを無限に生成することが出来ました。
##手順5 無限に生成する②ループさせる
Destoryを使った方法にはいくつか欠点があります。
- Destroyは処理が重く、小さいゲームではあまり問題にならないが、大きいゲームだとゲームが落ちる要因になりえる
- 生成するPlaneの大きさを毎回変えることが難しく、チャリ走のようなランダム性のあるゲームを作るのには不向き
これらの欠点をカバーするために、もう一つの方法を考えてみましょう。
イメージ図はこんな感じです。
こうすれば、Destroyの処理が無くてもステージを無限に生成できますね!
では、実際にやっていきましょう。
最初に、PlaneMoveScriptを消してください。
「え!今書いたばかりなのに!」と思う方、いると思います。
実は、スクリプトが複数になるとスクリプト間での変数受け渡しをしなければならず、複雑になってしまうので、ここでは一つのスクリプトで完結させます。
本来ならば役割ごとのスクリプトに分けた方が分かりやすいのですが……。
ということで、PlaneMoveScriptを消して、PlaneScriptを開いてください。
そして、以下のコードをコピペしてください。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlaneScript : MonoBehaviour {
public GameObject Plane;
GameObject[] step = new GameObject[10];
float speed = 20;
float disappear = -10;
float respawn = 30;
void Start() {
for (int i = 0; i < step.Length; i++) {
step[i] = Instantiate(Plane, new Vector3(4 * i, 0, 0), Quaternion.identity);
}
}
void Update() {
for (int i = 0; i < step.Length; i++) {
step[i].gameObject.transform.position -= new Vector3(speed * Time.deltaTime, 0, 0);
if (step[i].gameObject.transform.position.x < disappear) {
ChangeScale(i);
step[i].gameObject.transform.position = new Vector3(respawn, 0, 0);
}
}
}
void ChangeScale(int i) {
int x = (i + 9) % 10; //(i+9)を10で割った余りをxとする。
if (step[x].transform.localScale.y == 0.5) {
step[i].transform.localScale = step[x].transform.localScale + new Vector3(0, Random.Range(0, 2), 0);
}
else {
step[i].transform.localScale = step[x].transform.localScale + new Vector3(0, Random.Range(-1, 2), 0);
}
}
}
####このスクリプトの意味
#####Start関数
「前のものの高さを参照する」ためには、それぞれのPlaneに名前を付けるのが効果的です。
簡単に言えば、
「ひとつ前のものより10cm高くしろ」という命令より「太郎君より10cm高くしろ」という命令の方が伝わりやすいわけです。
#####Start関数
今回は配列を使って命名しました。
最初に10個のStepという名前の配列を用意し、Plane一つ一つにStep[0]、Step[1]、……と名前を付け、step[0]は(0,0,0)に、step[1]は(4,0,0)に、step[2]は(8,0,0)に……step[9]は(36,0,0)に、それぞれ出現しなさいと最初に言っています。
#####Update関数
for文を使って、step[0]からstep[9]までのすべてのPlaneに命令をしています。
命令の内容は以下の通りです。
- 1秒あたりx軸負方向にspeed(今回の場合は20)だけ移動させなさい
- もしx座標がdisappear(今回の場合は-10)より小さくなったら、以下のことをしなさい
- ChangeScale関数を呼び出す
- Planeの座標を(respawn(今回の場合は30),0,0)に変える
これでStepをループさせることが出来るようになりました。
#####ChangeScale関数
一言で言うと、ひとつ前のstepの高さを参照し、高さをランダムに変化させましょう、という意味です。
step[i]の高さを変える際には、ひとつ前であるstep[i-1]を参照すれば良いです。
しかし、i=0だった場合、i-1 = -1となって、step[i-1]が配列外参照となってしまいます。
int x = (i + 9) % 10; //(i+9)を10で割った余りをxとする。
従って、ChangeScale関数の最初で上記のようにすることで配列外参照を避けました。
また、stepの高さは負になってはいけないので、step[x]の高さが0.5だった場合はstep[i]はstep[x]より高くなるか、同じ高さとなります。それ以外の場合はstep[x]より高くなるか、同じ高さになるか、低くなるかは同確率で起こります。
補足として、Random.Range()は、()の間に含まれる値の内どれかをランダムで返すメソッドですが、float型で宣言するか、int型で宣言するかで戻り値の範囲が異なります。
型 | メソッドの書き方 | 戻り値の範囲 |
---|---|---|
float型 | Random.Range(float min, float max); | min≦戻り値≦max |
int型 | Random.Range(int min, int max); | min≦戻り値<max |
今回の場合はint型で宣言しているので、Random.Range(-1,2)は、-1,0,1のうちどれかをとるという意味になります。
以上でスクリプトの説明は以上となります。
実際に実行してみましょう。
以上のような挙動をしたら成功です。おめでとうございます。
成功しなかった人は、InspectorビューでPlaneを指定しているか確認してみてください。
##最後に
Unityを使ったステージ無限生成のやり方のQiita記事が探したところなかったので書いてみました。初めて書いたので、かなり読みにくい点もあったと思います。ここまでお付き合いいただき、ありがとうございました。
実は、このやり方にも欠点があります。
簡単に言えばTime.deltaTimeの精度があまり高くなく、disappearより小さいか否かの判定がかなり適当になっているため、徐々に足場がずれて穴が出来ていきます。
今回のやり方ではPlaneの長さ4.5に対し、Plane間の距離を4としてPlane同士の重なりを持たせることで誤魔化しています。
上記の欠点を克服する方法が分かったらまた新しく記事にしますので、よろしくお願いします。