Clusterのワールド作成をC#スクリプトで時短する
この記事は Cluster Creator #2 Advent Calendar 2022 12日目の記事です。
目次
はじめに
早速ですが、私は10月28日に開催された Cluster GAME JAM 2022 Autumnに参加し『Twinkle Flight』というワールドを作成・公開しました。
さて、このワールドではエントランスには3色のブロックが25×25=625個規則的に並べられおり、上空には12色の取得できる小さな星が一定の空間内に1000個ランダムに配置されています。そんな作業をUnityのHierarchyやInspectorの手入力では大変面倒です。
Cluster対応のUnity2021.3.4f1では、Unityエディタ上のInspector上に数式を入力することによって、複数のオブジェクトを規則的に並べることもできます。
数式を使ってオブジェクト配置を効率化しよう!(Creators Guideより)
しかしながら、この機能だけでは、エントランスの3色のブロックを625個並べることは困難です。
けれども、Unity標準のC#スクリプトを使用することによって、上記のことが可能になります。
ClusterではC#スクリプトは動作しませんが、Unityエディタ上では問題なく動作します。Cluster Creator Kitが導入されていようが動作します。そして、C#スクリプトによってワールド作成を大幅に時短することもできます。
2022年10月20日よりClusterではJavaScriptによるScriptable Itemが実装可能となったこともあって、スクリプトに関する知識も手に入れた人々も多いも思います。
とはいっても、C#とは無縁な方が多いと思います。先ほどにも記述した通りClusterはC#スクリプトが動作しないからです、UnityはClusterのワールド作成以外にもVRMアバターの改変、制作のために使われることもありますが、そんなアバター改変にもC#というものは滅多と登場しません。
だからこそ、今回は「C#何にも知らないよ!」という方々に向けて、C#スクリプトを0から説明したいと思います。
ちなみに、C#スクリプトの知識や技術がある方は「実践!C#スクリプトで楽々オブジェクト配置!」まで飛ばしても大丈夫です。
0から始めるC#スクリプト入門
C#スクリプトの作成
まず初めに動かすためのC#スクリプトを作成する必要があります。
スクリプトを作成するには、Assetsウィンドウを右クリックして「Create」→「C# Script」をクリックしてファイル名を入力します。この点はMaterialやAnimationを作成するのと同様です。
また、このアイコンをダブルクリックすると外部のスクリプトエディタが開かれます。以下のテキストが表示されたら無事スクリプト作成は成功です。5行目にある”public class xxxxx : MonoBehaviour”の”xxxxx”にはファイル名が代入されます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
関数ならびにイベント関数の理解
void なんとか()と書かれた後に{}が続く文のことを「関数」もしくは「メソッド」と呼びます。
また、その中でもUnity側から実行される関数のことを「イベント関数」と呼びます。スクリプト生成時に最初から用意されている「void Start(){}」や「void Update(){}」はイベント関数です。
「void Start(){}」はエディタ再生直後に実行され、{}内に記述された処理が行われます。Clusterワールド作成には、このイベント関数に処理を記述します。
「void Update(){}」は毎フレーム(標準的な60Hzのモニターで標準設定ならば、毎秒60回)呼ばれるイベント関数ですが、Clusterワールド作成では滅多と使いません。
C#スクリプトファイル生成時にあらかじめスクリプトが用意されていますが、処理がどこにもないので、何も起こりません。
早速「void Start(){}」イベント関数に処理を書きたいと思います。
Unityでは様々な挙動を生み出すための機能が用意されています。まず手始めに動作を確認するためによく使われる、Unityエディタのコンソールにメッセージを出力する「Debug.Log()」を使ってみましょう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
// UnityエディタのConsoleに"Hello World"というメッセージを出力する。
Debug.Log("Hello World");
}
// Update is called once per frame
void Update()
{
}
}
スクリプトを記述した後、Unityエディタの操作に移る前に、必ずスクリプトは保存しましょう。
スクリプトを動かす
基本的にC#スクリプトファイルは単体では動作しません。スクリプトを動作させるにはGameObjectにスクリプトファイルをアタッチする必要があります。
スクリプトファイルをアタッチする方法は以下の通りです。
スクリプトファイルをGameObjectにアタッチすれば、スクリプトを動作させる準備は完了です。
Unityエディタ上の再生ボタンを押します。
すると、Consoleウィンドウに以下のログが出力されます。
これは「UnityエディタがGameObjectにアタッチされたC#スクリプトを参考にして、再生直後にStart()イベント関数を実行し、その中に記述されたDebug.Log(“Hello World”)が実行された」ということです。
また、Debug.Log()のような処理を行う文のことも「メソッド」と呼びます。
こんな感じでC#スクリプトを動作させます。
変数を理解する
さて、C#スクリプトには変数という概念があります。
変数とは、コンピュータ内部で記憶される数値などの情報です。
変数は定義して使います。変数の定義のやり方は以下の通りです
変数の型 変数名 = 数値;
変数には「整数」「実数」そして「ベクトル」など様々な型があります。その中でも手始めに4つの変数の型を紹介します。
int // -1,0,1,2,3 などの整数
float // 3.14や2.72 などの実数
bool // trueかfalseか
string // 任意の文字列
変数の定義のやり方はそれぞれ以下の通りです。
int a = 144;
float b = 3.1415f; // C#ではfloat型の数値の末尾には「f」をつける
bool c = true;
string d = "cluster"; // 文字列の定義には「"」で囲む
なお、変数名は数字が使えますが、数字から始めてはいけません。また演算で使う記号なども使用することはできません。
それぞれの変数の用法が理解できたならば、Start(){}イベント関数に定義してみましょう。
void Start()
{
int a = 144;
float b = 3.1415f;
bool c = true;
string d = "cluster";
Debug.Log(a);
Debug.Log(b);
Debug.Log(c);
Debug.Log(d);
}
Unityエディタのコンソールに変数の値が出力されます。
演算を理解する
// in=int型、fn=float型、bn=bool型、sn=string型としてあらかじめ定義しています。
i2 = i1 + 144; // 変数に定数を加算する
i3 = i1 + i2; // 変数同士加算させる
i4 = i1 - i2; // 引き算させる
i5 = i1 * i2; // 掛け算「*」を使います。
i6 = i1 / i2; // 割り算(整数同士での割り算は割れる数だけ返します exp: 7 / 3 = 2
i7 = i1 % i2; // 割り算のあまり exp: 7 % 5 = 2;
i8 = (i1 + i2) * i3 // 3つ以上の変数の計算、実世界同様に掛け算優先されるので括弧も使えます。
f2 = f1 + 2.71828f; // 実数型に固有値を加算
f3 = f1 + f2;
f4 = f1 - f2;
f5 = f1 * ii; // 実数値に整数値を乗算する
b2 = !b1; // trueとfalseを反転させる
b3 = b1 && b2 // 論理積、両方がtrueの場合においてb3がtrue、それ以外はfalseになる。
b4 = b1 || b2 // 論理和、片方がtrueの場合においてb3がtrue、それ以外はfalseになる。
s2 = s1 + "world"; 文字列の連結
s3 = s1 + i1; // 文字列に整数を連結
代入演算子
「a = a + b」みたいに自身の値を用いて更新する計算が多用されるので、そのような場合において「代入演算子」という、以下の省略記法を使うことができます。
i1 += i2; // 「i1 = i1 + i2;」と同義、以下同様
i1 -= i2;
i1 *= i2;
i1 /= i2;
i1 %= i2;
f1 += f2;
f1 -= f2;
f1 *= f2;
f1 /= f2;
s1 += s2;
インクリメント、デクリメント
かなりの頻度で「a = a + 1」や「a+=1」が用いられます。この場合は「インクリメント(加算)」や「デクリメント(減算)」という、以下の省略記法を使うことができます。
a++; // インクリメント、「a = a + 1;」や「a += 1;」と同義
a--; // デクリメント、「a = a - 1;」や「a -= 1;」と同義
条件演算子
C#スクリプトには条件演算子というのがあり、int型やfloat型の変数を比較してbool型の変数にすることができます。
// in=int型、fn=float型、bn=bool型、sn=string型としてあらかじめ定義しています。
b1 = i1 == i2; // i1がi2と等しいか
b2 = i1 != i2 // i1がi2と等しくないか
b3 = i1 >= i2 // i1がi2より大きいもしくは等しいか
b4 = i1 > i2 // i1がi2より大きいか
b5 = i1 <= i2 // i1がi2より小さいもしくは等しいか
b6 = i1 < i2 // i1がi2より小さいか
配列を理解する
C#には複数の変数を格納できる「配列」というのがあります。
配列の定義は以下の通りです
変数の型[] 配列名 = new []{値0, 値1, 値2...};
int型の配列の例は以下の通りです。
int[] numbers = new[]{1,4,2,8,5,7};
配列に格納された値を取得する方法は以下の通りです。また、配列の番号は0から始まります。
配列名[int型の値];
また、以下の記述を行うことによって配列に格納された変数の総数を取得できます。
配列名.length;
それを踏まえた上で、配列のサンプルは以下の通りです。
int[] numbers = new[]{1,4,2,8,5,7};
int number = numbers[0]; // 配列の番号は0から始まる、numberには1が代入される
number = numbers[3]; // numberには8が代入される
number = numbers.length; // numbersに定義された変数の総数は6なので、6が代入される
number = numbers[9]; // 配列外の数値を指定するとエラーになる
分岐処理や繰り返し処理を理解する
C#をはじめ、必ずと言っていいほどスクリプトには分岐処理や繰り返し処理というのが存在します。
if文の理解
C#スクリプトには、条件もしくはbool型変数がtrueの時に実行される分岐処理「if文」というのが存在します。
if文の記法は以下の通りです。
if(b1)
{
Debug.Log("b1はtrueです");
}
ちなみに、if文には「else文」という条件がfalseの場合に代わりに実行される追記の記法もあります。
if(b1)
{
Debug.Log("b1はtrueです");
}
else
{
Debug.Log("b1はfalseです");
}
そして、elseの後にも条件を追加したelse if文というのもあります。
if(b1)
{
Debug.Log("b1はtrueです");
}
else if(i1 > 3)
{
Debug.Log("b1はfalseで、i1は3より大きいです");
}
else
{
Debug.Log("b1がfalseで、i1は3以下です");
}
繰り返し処理、for文
C#スクリプトでは繰り返し処理を行うための機能が搭載されており、中でもfor文というのが多用されます。for文の記法は以下の通りです。
for(変数定義; 継続条件; ループ後の変数の処理)
{
// ここに処理を書く
}
その中でもよく使われるのが以下のサンプルです。
for(int i = 0; i <= 100; i++)
{
Debug.Log(i);
}
これはfor文の中で使われるint型変数iが0となり、大括弧の中の処理が終わればiが1加算され、iが100以下であればループを続行する、という意味となります。
この処理を実行するとUnityエディタのコンソールに0~100の値が出力されるようになります。
for文は配列との相性がよく、以下の記法がよく多用されます。
int[] numbers = new[]{1,4,2,8,5,7};
// 配列を前から順番に全部取得して、コンソールに出力する
for(int i = 0; i < numbers.length; i++)
{
Debug.Log(numbers[i]); // 1,4,2,8,5,7と6行出力される
}
// 一定回数繰り返させて、配列の値を循環させ、コンソールに出力する
for(int i = 0; i < 100; i++)
{
Debug.Log(numbers[i % numbers.length]); // 1,4,2,8,5,7,1,4,2,8,5,7,1,4 と100行出力される
}
繰り返し処理、while文
条件がtrueである間、処理を繰り返す記法です。
while(継続条件)
{
}
なお、while文は条件が常にtrueとなってしまって、延々と処理が実行されることが頻繁にあります。これを「無限ループ」と呼び、発生するとUnityエディタはフリーズします。ゆえにwhile文を使用する際は慎重に扱う必要があります。
これにて、C#の基本的な文法の紹介は以上です。ここからはUnityの機能を紹介したいと思います。
UnityエディタからGameObject情報を格納する情報、インスタンス変数を作成する。
先ほど変数の定義などをStart(){}イベント関数の内部で行いましたが、この定義はイベント関数の内部ではなく、クラスの内部で行うことができます。
ただし、クラス内部で変数を定義する場合はアクセス修飾子というものを使います。アクセス修飾子には「private」と「public」などがあり、それぞれ外部のスクリプトから変数にアクセスできるかを指定するものです。
記法は以下の通りです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
// classを定義する{}の直下に記入
アクセス修飾子 変数の型 変数名 = 初期値; // 初期値は省略してもいい。
// Start is called before the first frame update
void Start()
{
// この中には定義しない。
}
// 省略
}
なお、今回は以下のようにします
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
// classを定義する{}の直下に記入
public GameObject prefab;
// Start is called before the first frame update
void Start()
{
// この中には定義しない。
}
// 省略
}
このようにクラス内部で定義された変数のことを「クラス変数」もしくは「インスタンス変数」と呼び、publicなどのアクセス修飾子の前にstaticがついたものをクラス変数、そうでない変数をインスタンス変数と呼びます。
また、上記のようにアクセス修飾子がpublicなインスタンス変数を定義すると、UnityエディタのInspectorから値を設定できるようになります。
そして「GameObject」という型を使うことによって、UnityエディタのHierarchy上にあるオブジェクトの情報を設定できるようになります。
オブジェクトの設定方法は、Cluster Creator Kitのギミックと同様、Hierarchyからドラッグ&ドロップします。
Inspectorのスクリプトにオブジェクトを設定するには、Hierarchyからオブジェクトをドラッグ&ドロップします。
さて、GameObjectを設定できるインスタンス変数prefabを定義して、Inspector上に何かしらのオブジェクトを設定したところで、何かしたいと思います。
Unityには、Scene上にオブジェクトを生成する「Instantiate()」というメソッドがあります。
Instantiate(GameObject prefab);
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
public GameObject prefab;
// Start is called before the first frame update
void Start()
{
Instantiate(prefab);
}
}
その後、Unityエディタで実行すると、オブジェクトが生成されます。
そして、複製されたオブジェクトは、Unityエディタの再生を停止すると、消えます。この点は再生時にオブジェクトの位置や回転などを編集して再生を停止すると、再生前の状態に戻るのと同様です。
ならば「オブジェクトが消えてしまうなら、ワールド作成できないじゃん!」と思うかもしれませんが、以下の方法を使うことによって、生成されたオブジェクトを残せます。
- 複製されたオブジェクトを選択して、Ctrl+Cなどでコピーする
- Unityエディタの再生を停止する
- Hierarchy上にCtrl-Vなどでペーストする
オブジェクトの子として、オブジェクトを複製する
先ほどのInstantiateメソッドは、Hierarchyに直接複製するものです。Instantiate()メソッドの記法を変更することで、オブジェクトの子として複製することもがきます。
オブジェクトを子オブジェクトとして複製する方法は以下の通りです。
Instantiate(GameObject prefab, Transform parent);
当たり前ですが、オブジェクトを子オブジェクトとして複製するには、親オブジェクトを指定する必要があります。ただ、その際はGameObjectではなくTransformという変数を用います。
以下のインスタンス変数を追加します。
public GameObject prefab;
public Transform parent; // 追加する
Transformとは、GameObject生成時に必ずInspector上の最上にある位置や回転の情報を格納するいつものコンポーネントです。
すべてのオブジェクトに存在するので、GameObjectと同様、Inspectorにアタッチできます。
それを踏まえた実例は以下です。今回はfor文を使って10個生成してみます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
public GameObject prefab;
public Transform parent;
// Start is called before the first frame update
void Start()
{
for (int i = 0; i < 10; i++)
{
Instantiate(prefab, parent);
}
}
}
結果はこのようになります。
位置と回転を指定してオブジェクトを複製する
先ほどのオブジェクト複製では、必ず位置がx=0,y=0,z=0となり、回転もx=0,y=0,z=0になり、10個のオブジェクトが完全に重なった状態で複製されてしまいます。
位置と回転を指定して、指定した親オブジェクトの子としてオブジェクトを複製するコードは以下です。
Instantiate(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent);
位置はx,y,zの3つの実数で構成されていますので、Vector3という型を使います。Vector3は「構造体」という特殊な型で、intやfloatとは違って、定義が特殊です。
Vector3型変数の定義は以下の通りです。
// fx,fy,fzはあらかじめfloat型として定義
Vector3 position = new Vector3(fx, fy, fz);
Vector3 position = new Vector3(1.0f, 2.0f, -3.0f);
そして、回転の情報もInspector上ではx,y,zの3実数で構成されていますが、回転はQuaternionという型を使います。この型はVector3と同じく「構造体」で、Vector3のようにnew Quaternion()で定義することができますが、Quaternionはw,x,y,zの4実数で構成されており、4実数設定する必要があります。そして、初学者にとって4実数で回転情報を定義するのは複雑です。
そんなQuaternionには「オイラー角」という4変数を3変数に変換する概念があり、その3変数は普段UnityエディタのInspector上で扱っているx,y,zです。また、Quaternionには「Quaternion.Euler()」を使うことによって、x,y,zにて回転情報を定義することができます。
オイラー角を用いた、Quaternion型変数の定義は以下の通りです。
// fx,fy,fzはあらかじめfloat型として定義
Quaternion rotation = Quaternion.Euler(fx, fy, fz);
Quaternion rotation = Quaternion.Euler(90f, 180f, 270f);
ちなみに、回転がx,y,zともに0の場合は「Quaternion.Identity」が使えます。
Quaternion rotation = Quaternion.identity; // Quaternion.Euler(0f,0f,0f)と同義
そんな感じで、位置と回転を指定してオブジェクトを10個作成するサンプルは以下です。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SampleScript : MonoBehaviour
{
public GameObject prefab;
public Transform parent;
// Start is called before the first frame update
void Start()
{
for (int i = 0; i < 10; i++)
{
float rotationY = i * 30f;
Vector3 position = new Vector3(i, i, i);
Quaternion rotation = Quaternion.Euler(0f, rotationY, 0f);
Instantiate(prefab, position, rotation, parent);
}
}
}
これにて、C#スクリプトの解説は以上です。
実践!C#スクリプトで楽々オブジェクト配置!
さて、C#スクリプトの記法ならびにUnityエディタ上で動作させる方法を理解できたのであれば、上記のワールドの実例を元にC#スクリプトを使ってワールドのオブジェクトの配置したいと思います。
今回の事例は以下の2通りです
- 計3色のブロックを25×25=625個規則的に配置する
- 計12色の星を一定空間内に1000個ランダムに配置する
計3色のブロックを25×25=625個規則的に配置する
当ワールドに入るとカラフルなブロックがたくさん並べられてた土台があります。まず手始めにこの土地をC#スクリプトで配置したいと思います。
土地の仕様は以下の通りとなっております。
- ブロックはプリミティブなCubeで作られており、Scaleはx=0.72、y=0.04、z=0.72である。
- ブロックの数は25×25=625
- ブロックの色は3色、ゆえにマテリアルも3つ
- 配置間隔は0.8で、中央のブロック位置がx=0、y=-0.1、z=0に配置する
- ブロックの色は規則的に、見栄え良く配置する
上記の仕様に基づいてマテリアルを3個、プリミティブなCubeを3つ作成します。
インスタンス配列を作成する
先ほどプリミティブなCubeを3つ作成したら、スクリプトに3つのオブジェクト情報を設定する必要があります。
愚直に考えれば、以下のコードが考えられます。
public GameObject prefab1;
public GameObject prefab2;
public GameObject prefab3;
しかしながら、上記のコードは「仮にオブジェクト数が100個になった時に、定義が100個必要になる」や「今後の処理で分岐処理が必要になる」という問題を抱えます。
この場合は配列というものを使います。配列はインスタンス変数でも定義可能で、GameObjectに対しても配列にできます。
以下のコードで、インスタンス変数(インスタンス配列)を定義します。
public GameObject[] prefabs = new GameObject[3]; // 配列の場合は、配列名を複数形にすることが多い
上記のように、インスタンス配列をpublicで定義すると、Inspectorに設定項目が複数個出現します。
オブジェクトの名前を変更する
「Instantiate()」によって生成されたオブジェクトは名前が必ず「オブジェクト名(clone)」となります。複数個生成した場合、全ての名前が同じになるのが気になる方もいるかもしれません。
ちなみに、Instantiate()は以下の書き方をすることで複製されたオブジェクトの情報が代入されます。
GameObject obj = Instantiate(prefab); // 生成したオブジェクトの情報が格納される。
そして、以下のコードによって、複製されたオブジェクトの名前を変更できます。
GameObject obj = Instantiate(prefab);
obj.name = "オブジェクト名";
スクリプトを完成させる
これらを踏まえて作成されるスクリプトは以下の通りです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FloorMapper : MonoBehaviour
{
public GameObject[] prefabs = new GameObject[3];
public Transform parent;
private void Start()
{
int i = 1;
for (int x = 0; x < 25; x++)
{
for (int z = 0; z < 25; z++)
{
float positionX = x * 0.8f - 9.6f;
float positionZ = y * 0.8f - 9.6f;
GameObject floor = Instantiate(prefabs[i % 3], new Vector3(positionX, -0.1f, positionZ), Quaternion.identity,
parent);
floor.name = "Floor (" + i + ")";
i++;
}
}
}
}
実例「計3色のブロックを25×25=625個規則的に配置する」は、これにて以上です。
計12色の星を一定空間内に1000個ランダムに配置する
さて、625個のブロックを規則的に配置したら、今度は上空に1000個の星を配置したいと思います。
星々の仕様は以下の通りとなっています
- 星には独自のメッシュを採用する
- 星の数は1000個
- Scaleはxyz共通でそれぞれ4,6,8,10のいずれか
- x=-100~100、y=10~100、z=-100~100の範囲内でランダムに配置する
- 星の色は12色、ゆえにマテリアル数12
- 星はAnimatorによってy軸に回転し、その他Itemで取得可能オペレーションを定義する
ちなみに、今回の星は先ほどのブロックとは違って取得可能なアイテムとして扱いますのでItemならびにDestroyItemGimmickなどをアタッチしています。回転もするのでAnimatorのコンポーネントもアタッチしています。
けれども、そのオブジェクトで異なるのは位置情報とサイズそしてマテリアルのみです。そして用意するマテリアルファイルの数は12個です。
先ほどのブロックと同様に複製しようとするならば、マテリアル以外の情報が同一であるオブジェクトを12個用意する必要があります。
ブロックみたいに3個ならともかく、同じようなオブジェクトを12個も生成するのは面倒なことです。しかも今回はItemコンポーネントも追加し、ギミックやオペレーションも追加します。ギミックやオペレーションのパラメータに不具合があり、想定外の動作がおこなった場合において、修正するのに12個分のオブジェクトのコンポーネントのパラメータを変更する必要があり、大変面倒なことが起こりえます。
だからといって「C#スクリプトでマテリアルの情報を変更することはできるのか」というと、断然「はい」です。変更することは可能です。
とはいえメッシュのマテリアル情報を格納するフィールドはMeshRendererというコンポーネントにあります。GameObjectは生成時、必須であるTransform以外のコンポーネントは存在しないので、GameObject変数にマテリアル情報を変更するプロパティは存在しません。
ゆえにGameObjectにアタッチされているコンポーネントを取得する必要があります。その方法は以下の通りです。
// objはInstantiateなどで代入されたGameObject
コンポーネント名 component = obj.GetComponent<コンポーネント名>();
パラメータの変更方法などは、取得したコンポーネントによって様々です。なお、コンポーネントというのはUnity標準のものだけでもたくさんあります。実は自作のC#スクリプトファイルですらもコンポーネントになります。強いていうならばCluster Creator Kitが用意したギミックなどもコンポーネントになります。
なおゲームオブジェクトに取得対象のコンポーネントがひとつもアタッチされていない場合、コンポーネント変数がnullになります。この状態でパラメータなどを変更するとエラーが発生します。
ちなみに、MeshRendererというコンポーネントを取得し、マテリアル情報を変更する処理は以下です。
// objはInstantiateなどで代入されたGameObject
MeshRenderer meshRenderer = obj.GetComponent<MeshRenderer>();
meshRenderer.material = material;
また、Cluster Creator Kitが用意したコンポーネントも取得できますが、2022年12月12日現在、C#スクリプトを用いて、Cluster Creator Kitのコンポーネントのパラメータを変更することはできません。
ただ、マテリアルを変更するためには、元となるマテリアルの情報が必要です。そのためにはマテリアルの情報を定義するには「Material」という変数を使います。
public Material[] materials = new Material[12];
マテリアルをInspectorに定義するのは、アセットからマテリアルファイルをドラッグ&ドロップします。
Scaleを変更する
オブジェクトの必須コンポーネントであるTransformには位置(Position)と回転(Rotation)そして縮尺(Scale)の3つがありますが、不思議なことにInstantiate()では位置と回転は指定できますが、縮尺を指定することができません。
縮尺を変更する方法は以下です。
// objはInstantiateなどで代入されたGameObject vector3はVector3型変数
obj.transform.localScale = vector3;
乱数を取得する
「UnityEngine.Random.Range()」という乱数を取得するメソッドがあります。
使い方は以下の通りです。
f3 = UnityEngine.Random.Range(f1, f2); // float型の乱数、f1以上、f2以下の実数が代入される
i3 = UnityEngine.Random.Range(i1, i2); // int型の乱数、i1以上、i2"未満"の整数が代入される
i3 = UnityEngine.Random.Range(0, 3); // 0,1,2のいずれかが代入される、3が代入されることがない
乱数にはfloat型とint型の両方があり、Range()の引数の型によって決まります。また、float型とint型では振る舞いが異なり、int型の場合は未満に変わります。
そして、上記のメソッドを使って配列からランダムに値を取り出す、多用されるスクリプトがあります。
// 配列の要素をランダムに取得するテクニック
// numbersはint型の配列として定義済
int number = numbers[UnityEngine.Random.Range(0, numbers.length)];
スクリプトを完成させる
これらを踏まえて作成されるスクリプトは以下の通りです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StarGenerator : MonoBehaviour
{
public GameObject prefab;
public Material[] materials = new Material[12];
public Transform parent;
private void Start()
{
float[] sizes = new[] {4.0f, 6.0f, 8.0f, 10.0f};
for (var i = 0; i < 1000; i++)
{
float positionX = UnityEngine.Random.Range(-100.0f, 100.0f); // x座標は-100~100
float positionY = UnityEngine.Random.Range(10.0f, 100.0f); // y座標は10~100
float positionZ = UnityEngine.Random.Range(-100.0f, 100.0f); // z座標は-100~100
Vector3 position = new Vector3(positionX, positionY, positionZ); // 先ほど定義した3つのfloat型変数を用いて、Vector3変数作成
GameObject star = Instantiate(prefab, position, Quaternion.identity, parent);
star.name = "Star (" + i + ")"; // 生成されたオブジェクトの表示名を「Star (i)」に変更
float scale = sizes[UnityEngine.Random.Range(0, sizes.Length)]; // 先ほど定義したfloat型配列から1つの値を任意に取得
star.transform.localScale = new Vector3(scale, scale, scale); // スケールを設定
MeshRenderer meshRenderer = star.GetComponent<MeshRenderer>();
meshRenderer.material = materials[i % materials.Length];
}
}
}
実例「計12色の星を一定空間内に1000個ランダムに配置する」は、これにて以上です。
そして「Clusterのワールド作成をC#スクリプトで時短する」の記事紹介も、これにて以上です。
さいごに
C#スクリプトが動作しないClusterでも、C#スクリプトでワールド生成を時短できることをお伝えしました。
ご紹介したC#スクリプトはごく一部ですが、上記の内容だけでも十分ワールド作成に役に立つと思います。とはいえ、C#スクリプトを用いてオブジェクトを正しく配置するのは慣れが必要かもしれませんが「C#スクリプトによってワールド作成が楽になったよ」と思えるようになったら、筆者は幸いです。
それでは、よき創作ライフを!