WisdomSoft - for your serial experiences.

スクリプトによるオブジェクト制御

「第3回 渋谷Unity技術勉強会-Unity×Html/Unity 2D」で発表した資料です。

オブジェクトとコンポーネント

前回に引き続き、株式会社ハッチアップ主催、渋谷セルリアンタワーにあるGMOインターネット株式会社のオフィス内て2012年3月29日に開催された「第3回 渋谷Unity技術勉強会-Unity×Html/Unity 2D」にて、技術セッションのスピーカーを担当させていただきました。

今回は技術的なデモンストレーションを中心に C# スクリプトを用いた Unity ゲームオブジェクトの制御についてです。Unity エンジンがオブジェクトをどのように管理し、スクリプトからどのような制御が可能なのかを解説します。すでに Unity でゲーム開発をされている方にとっては基本的な内容となりますが、ここを理解しなければ Unity スクリプトで何ができるのか、何ができないのかを判断できません。

勉強会資料

Unity スクリプトは非常に柔軟で強力な機能を提供します。各種コンポーネントを理解することで、実行時にスクリプトからユーザー生成コンテンツなど高度な制御が可能となります。

実行時コンポーネント操作

会場で行ったデモンストレーションのコードを、いくつか紹介します。Unity のゲームオブジェクトに対して、スクリプトからコンポーネントを自由に追加したり削除したりできます。シーン内のあらゆるゲームオブジェクトはコンポーネントによって役割を決定します。スクリプトからコンポーネントを操作することで、ゲームオブジェクトの振る舞いを実行時に制御できるようになります。

以下のコードは極端な例ですが、コンポーネントの追加は文字列からも行えるため、ユーザーが入力した文字列に一致するコンポーネントを任意のオブジェクトに追加することも可能です。

コード1
using UnityEngine;

public class ComponentEditor : MonoBehaviour 
{	
	private string targetName = "";
	private string componentName = "";
	
	void OnGUI()
	{
		Rect uiRect = new Rect(10, 10, 200, 20);
		GUI.Label(uiRect, "Target Name:");
		
		uiRect = new Rect(150, 10, 200, 20);
		targetName = GUI.TextField(uiRect, targetName);
		
		uiRect = new Rect(10, 40, 200, 20);
		GUI.Label(uiRect, "Component Name:");
		
		uiRect = new Rect(150, 40, 200, 20);
		componentName = GUI.TextField(uiRect, componentName);
		
		uiRect = new Rect(10, 70, 100, 30);
		if(GUI.Button(uiRect, "Add")) buttonPushed();
	}
	
	private void buttonPushed()
	{
		GameObject target = GameObject.Find(targetName);
		target.AddComponent(componentName);
	}
}
実行結果
コード1 実行結果

コード1は「Add」ボタンを押すと「Target Name」テキストボックスに入力した名前のゲームオブジェクトに、「Component Name」 テキストボックスに入力した名前のコンポーネントを追加します。

実行時ポリゴン生成

Unity を用いた一般的な開発工程は、クリエイターが制作した 3D モデルデータをアセットとして読み込みますが、スクリプトから実行時に頂点配列を設定し、ポリゴンを描画できます。以下のコードは 3 つの頂点とインデックスからなる 1 つの三角形プリミティブをスクリプトから生成しています。

コード2
using UnityEngine;

public class CreateTriangle : MonoBehaviour 
{
	private GameObject triangleObject;

	void Start () 
	{
		triangleObject = new GameObject();
		triangleObject.AddComponent<MeshFilter>();
		triangleObject.AddComponent<MeshRenderer>();
		
		MeshFilter meshFilter = triangleObject.GetComponent<MeshFilter>();
		Mesh mesh = new Mesh();
		
		Vector3[] vertices = new Vector3[]
		{
			new Vector3(0, 1, 0),
			new Vector3(1, 0, 0),
			new Vector3(-1, 0, 0)
		};
		int[] triangles = new int[] { 0, 1, 2 };
		
		mesh.vertices = vertices;
		mesh.triangles = triangles;
		meshFilter.mesh = mesh;
	}
}
実行結果
コード2 実行結果

これを応用して、複数の分割されたメッシュの合成や、アルゴリズムによる数学的な構造の物体の作成などが可能となります。また、Unity が標準で対応していないデータをファイルやネットワークから読み込み、メッシュとして描画できます。以下のコードは MMD のモデルデータである PMD ファイルから頂点データを読み込んでメッシュに設定します。

コード3
using System.IO;
using UnityEngine;

public class LoadPMD : MonoBehaviour
{
	[SerializeField]
	private string source;
	
    void Start()
    {
        FileStream stream = File.Open("assets/" + source + ".pmd", FileMode.Open);
        stream.Seek(283, SeekOrigin.Begin);

        BinaryReader reader = new BinaryReader(stream);
        int vertexCount = reader.ReadInt32();
        Vector3[] vertices = new Vector3[vertexCount];
        Vector3[] normals = new Vector3[vertexCount];
        Vector2[] uv = new Vector2[vertexCount];
        for (int i = 0; i < vertexCount; i++)
        {
            float x = reader.ReadSingle();
            float y = reader.ReadSingle();
            float z = reader.ReadSingle();

            float nx = reader.ReadSingle();
            float ny = reader.ReadSingle();
            float nz = reader.ReadSingle();

            float u = reader.ReadSingle();
            float v = reader.ReadSingle();

            /* short bone1 = */ reader.ReadInt16();
            /* short bone2 = */ reader.ReadInt16();

            /* byte weight = */ reader.ReadByte();
            /* byte edge = */ reader.ReadByte();

            vertices[i] = new Vector3(x, y, z);
            normals[i] = new Vector3(nx, ny, nz);
            uv[i] = new Vector2(u, v);
        }

        int faceCount = reader.ReadInt32();
        int[] triangles = new int[faceCount];
        for (int i = 0; i < faceCount; i++)
        {
            short vertexIndex = reader.ReadInt16();
            triangles[i] = vertexIndex;
        }
        
        stream.Close();

        Mesh mesh = new Mesh();
        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.normals = normals;
        mesh.uv = uv;
        GetComponent<MeshFilter>().mesh = mesh;
    }
}
実行結果
コード3 実行結果

コード3は source フィールドに設定されたファイル名から PMD ファイルを読み込み、このスクリプトがアタッチされているゲームオブジェクトの MeshFilter コンポーネントに、読み込んだ頂点データを設定します。

実行時テクスチャ生成

スクリプトから任意のサイズのテクスチャを生成し、色の配列からピクセルを設定できます。数学的な手法を用いたフラクタルなどの図形描画や、複数のテクスチャを実行時に合成するなどの応用が可能です。

コード4
using UnityEngine;

public class RuntimeTextureInit : MonoBehaviour
{
	private Texture2D texture;
	private Color[] data;
	
	void Start () 
	{
		texture = new Texture2D(32, 32);
		
		data = new Color[texture.width * texture.height];
		for(int i = 0 ; i < data.Length ; i++)
		{
			data[i] = new Color(Random.value, 0, 0);
		}
		
		texture.SetPixels(data);
		texture.Apply();
		
		renderer.material.mainTexture = texture;
	}
}
実行結果
コード4 実行結果

コード4は赤要素の強さに乱数を用いた 32 x 32 ピクセルのテクスチャを立方体のマテリアルに設定しています。

実行時地形生成

高さの情報を持つハイトマップから複雑な地形オブジェクトを描画する Terrain コンポーネントをスクリプトから操作すれば、乱数などを用いて実行するたびに地形が変化するゲームを実現できます。Terrain コンポーネントの地形を編集するには TerrainData オブジェクトを設定します。TerrainData クラスは地形の高さを表す 2 次元 float 配列によるハイトマップを持ちます。

コード5
using UnityEngine;

public class RandomTerrainSample : MonoBehaviour 
{
	private GameObject terrain;
	private Terrain terrainComponent;
	private TerrainCollider terrainCollider;
	private TerrainData terrainData;
	
	void Start ()
	{
		int mapSize = 128;
		float[,] heightMap = new float[mapSize,mapSize];
		for(int h = 0 ; h < mapSize ; h++)
		{
			for(int w = 0 ; w < mapSize ; w++)
			{
				heightMap[w, h] = Mathf.Sin(w / 6.0F) / 2 + 0.5F;
			}
		}
		
		terrain = new GameObject();		
		terrain.AddComponent<Terrain>();
		terrain.AddComponent<TerrainCollider>();
		
		terrainComponent = terrain.GetComponent<Terrain>();
		terrainCollider = terrain.GetComponent<TerrainCollider>();
		
		terrainData = new TerrainData();
		terrainData.size = new Vector3(800, 400, 800);
		terrainData.heightmapResolution = mapSize;
		terrainData.SetHeights(0, 0, heightMap);
		
		terrainComponent.terrainData = terrainData;
		terrainCollider.terrainData = terrainData;
	}
}
実行結果
コード5 実行結果

コード5は Sin() 関数を用いた正弦波でハイトマップを作成しています。実行結果のように波の地形が描画されます。ハイトマップは 0.0 が最も低く、1.0 が最も高い位置を表すため 0.0 ~ 1.0 に収まるように数を調整してください。

これを応用して、複数の地形生成アルゴリズムを組み合わせれば、人間の手作業による編集を行うことなく、実行時に毎回新しいゲームを構築できます。以下は、乱数を用いてハイトマップを作成し、適度にぼかしをかけた状態の地形です。人の手作業で同様の地形を作るのは手間がかかりますが、プログラムならば一瞬です。

図1 乱数で生成された地形
図1 乱数で生成された地形

乱数を用いてプログラムが自動的にゲームを生成する仕組みは、制作者にとってデータ入力や調整など手間のかかる作業を自動化できるメリットがあります。一方で、ゲームバランスとの調整や、製作者にとっても予測できない組み合わせが発生する可能性があるため、ゲームの仕様に合わせて適切な場面で活用してください。

関連文書