Unityで開発するために、C#の基礎を知る。やりながら調べていく、はじめの一歩を作る。
チーム開発するうえで、知識をそろえる。
AtCoder Beginner Contest 095の問題を1つ以上C#で解く。
AtCoderに問題を提出することは強制しない。(登録が必要なため)
今日やってほしいのは、C#への"慣れ"の獲得の第一歩なので、何かしら書ければそれでよい。
Basic & for Unity => 今日のうちに通す 習得してほしい基礎
Append => 時間ありそうなら話す 知ってるとちょっと幸せかもしれない話
- 型と数
- 条件分岐と反復処理
- 名前空間
- コレクション
- クラス
- 継承とインターフェース
- 列挙型
- 構造体
- MonoBehaviour
- イベント関数と実行順
- 属性
- コーディングルール
- イベント
- コルーチン(Unity)
- LINQ
C#の雰囲気をつかむため、ハローワールドを見てみる。
Webからコードの実行ができる。言語を"C#","mcs HEAD"に設定する。
Visual Studioなど、自前の環境があるなら、それでよい。
Hellow Worldとコンソール上に出力するプログラムを作る。
// A Hello World! program in C#.
using System;
namespace HelloWorld
{
class Hello
{
static void Main()
{
Console.WriteLine("Hello World!");
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
Hello World!
Press any key to exit.
型は、大まかに以下のような分類ができる。
組み込み型 | ユーザー定義型 | |
---|---|---|
値型 | 単純型 | 構造体 列挙型 |
参照型 | 文字列型 オブジェクト型 | クラス デリゲート |
値型:値をスタックに確保。代入時にはコピーが作られる。
参照型:値をヒープに確保。代入時には参照を渡すだけ。
組み込み型:言語に予め組み込まれている型
ユーザー定義型:言語を使うプログラマが作る型
それぞれ、具体的に以下のような型がある。
組み込み型 | ユーザー定義型 | |
---|---|---|
値型 | int,float,bool | UnityEngine.Color |
参照型 | string,object | UnityEngine.MonoBehaviour |
C#でも、変数、定数を定義できる。
変数とは、C++/Java同様数値の入れ物である。C++との差分は以下のようなものがある。
- 初期化していない変数を参照するとコンパイルエラーとなる。
- 変数のスコープは、識別子を囲むブロック。
定数を定義するには、"const","readonly"キーワードを使用する。それぞれの意味は以下の通り。
- const:コンパイル時にリテラルに置き換わる。宣言時に初期化しなければならない。
- readonly:読み込み専用の変数を定義する。宣言時またはコンストラクタで初期化しなければならない。
なので、コンパイル時に値が確定しないような型の定数定義に"const"は使えない。
また、"readonly"は、それを付けた変数のみに適用されるため、そこから参照できる値は書き換え可能。
#define などで定数宣言したくなるだろうが、C#ではそのような使い方はできない。
C#での数値リテラルは、接尾辞をつけて型を指定できる。
たとえば、floatの"1"なら"1f"doubleの"1"なら"1d"というように指定できる。
ローカル変数の型を"var"とすることで、代入されるものから型を推測してくれる。
これは動的に型が変わるわけではなく、コンパイラが型を決定するだけである。たとえば、stringで初期化した"var"で宣言した変数は、string型の変数になる。
値型にnullを許容させることができる。これによって、"未割り当て"の状態を表現しやすくなる。
実際には、System.Nullable構造体のインスタンスが生成されている。そのため、null許容型も値型である。とはいえ、"null許容型"のnull許容型は宣言できない。
using System;
namespace HelloWorld
{
class Hello
{
// 読み込み専用 初期化以外の代入ができない
static readonly int readonly_n;
static Hello(){
//初期化
readonly_n = 111;
}
static void Main()
{
int n = 2;
float f = 1.1f; // 1.1では、double型になるので、fをつける
string str = "newgame!";
var interStr = "newgame!!2"; //型推論 初期化した値の型になる
int? nullableN = null; //null許容型 値型にnullが入れられるようになる
// 定数 コンパイル時にリテラルに置き換わる
const int CONST_N = 10;
//$"{変数名}"で、文字列の中に変数を埋め込める
Console.WriteLine($"{n},{f},{str},{interStr},{nullableN}");
Console.WriteLine($"{CONST_N},{readonly_n}");
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
}
2,1.1,newgame!,newgame!!2,
10,111
Press any key to exit.
if,switchともに、記法はC++/Javaと同様。
C#でのif文では、bool型の値を渡す。
C++では、たとえば、"0"という数リテラルでも"偽"として扱えたが、それはC#だとコンパイルエラーとなる。
switch文では、 caseの後にbreakをつけることを強制する。 ウソ。次のcaseラベルに到達できる処理の流れがないことを強制する。breakでなくともreturnでもよい。
C++では、たとえば、breakを書かないことでcase内の処理を上から下になめることができたが、C#ではコンパイルエラーとなる。
これは、どちらもC++でありがちな"コンパイルは通るが期待する動作にならない"ケースをコンパイルエラーにしようという思想のもとに作られている。
for,while,do-whileいずれもC++と同様に使用可能。ただし、ifと同様に条件にbool型を渡さなければならない。
forの拡張として、foreachがある。C++の範囲for/Javaの拡張forに相当するもの。
機能はコレクションの列挙。配列やリストを頭からなめることが可能。列挙中にコレクションの操作(追加、削除等)を行うと、例外を投げる。
using System;
namespace HelloWorld
{
class Hello
{
static void Main()
{
Console.WriteLine("Start For loop");
//var 型推論 この場合はint
for(var i = 1; i <= 15; i++){
if(i%3==0 && i%5==0){
Console.WriteLine("FizzBuzz");
}
else if(i%3==0){
Console.WriteLine("Fizz");
}
else if(i%5==0){
Console.WriteLine("Buzz");
}
else{
Console.WriteLine(i);
}
}
Console.WriteLine("Start While loop");
//"i"はforのスコープ上にあるので宣言できない
int j = 1;
while(j <= 4){
switch(j){
case 3:
Console.WriteLine("Fizz");
break; //省略不可
default:
Console.WriteLine(j);
break;
}
j++;
}
Console.WriteLine("Start Foreach loop");
int[] hoge = new int [] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
// hogeの要素を一つずつ取り出して、nに代入
foreach(var n in hoge){
string str = (n%3==0) ? "Fizz" : ""; //三項演算子 (条件式) ? 真の値 : 偽の値
str = (n%5==0) ? $"{str}Buzz" : str;
str = (str.Length > 0) ? str : $"{n}";
Console.WriteLine(str);
}
}
}
}
Start For loop
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
Start While loop
1
2
Fizz
4
Start Foreach loop
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
C#でも、C++/Java同様に名前空間が存在する。機能も同様に、クラス等名前の衝突を防ぐことにある。
namespaceキーワードで名前空間を作成、拡張できる。
usingディレクティブで他の名前空間を参照可能になる。
C++と違い、名前空間名とクラス名の間は"."でつなげばよい。また、Javaと違い、C#の名前空間は物理的なフォルダ分けによらない。
//名前空間の使用
using System;
using System.Collections.Generic;
//名前空間の作成
namespace HelloWorld
{
}
いわゆる配列。C#の場合、宣言の形がC++と違い、[]を型名の後にもつける必要がある。
多次元の配列を作る場合、多次元配列とジャグ配列(入れ子配列)の2つが作れる。
多次元配列は、[]の中に、','を入れることで宣言できる。いわゆる多次元配列なので、大きさが四角く広がっていく。
一方で、ジャグ配列では、[][]のように、[]を重ねていく。ジャグ配列は配列の配列なので、中の配列サイズはまちまち。
リスト、辞書はC++でのVectorやMapのようなもの。
List<T>のTには、そのコレクションが扱う型が入る。C#ではジェネリックと呼ばれる。
Array,List,Dictionaryのほかにも、値を格納するデータ構造は様々ある。これらがすべてforeachで列挙できるのは、どれもIEnumerableというインターフェースを実装しているため。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
class Hello
{
static void Main()
{
//多次元配列 正方の配列
int[,] rectangularArray = new int [,]{{1,2,3},{4,5,6}};
foreach(var n in rectangularArray){
Console.WriteLine(n);
}
//ジャグ配列 配列の配列
int[][] jaggedArray = new int[][]{
new int [] {1,2},
new int [] {3,4,5,6},
};
foreach(var j in jaggedArray){
Console.WriteLine(j);
}
//List<T>で宣言 Tにはリストの要素の型が入る
List<int> list = new List<int>{1,2,3,4};
list.Add(6);
list.Add(5);
list[0] = 0;
foreach(var n in list){
Console.WriteLine(n);
}
//key-value Dictionary<key,value>で、それぞれの型を指定する
Dictionary<string,int> dict = new Dictionary<string,int>();
dict["aoba"] = 18;
dict["nene"] = 17;
foreach(var c in dict){
Console.WriteLine(c);
}
}
}
}
1
2
3
4
5
6
System.Int32[]
System.Int32[]
0
2
3
4
6
5
[aoba, 18]
[nene, 17]
AtCoder Beginner Contest 095の問題を、C#を用いて一つ以上解いてみましょう。
AtCoderに提出する場合、コンパイル環境が最新でないことに注意しましょう。
C#でも、Java/C++同様にクラスを作ることができる。
それぞれと(ほぼ)同等の機能がある。
記法は使用例を参照。
C++と違い、ヘッダファイルに宣言、ソースに実装というような分割はない。
また、C++では変数宣言時にインスタンスが生成されたが、C#の場合は明示的にnewでインスタンスを生成、格納する必要がある。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
//class宣言 アクセス修飾子、staticをつけることもできる
class Book
{
//フィールド クラスの持つ状態、情報
private string _title; //private:クラスの外側から見られない
public string Isbn; //public:クラスの外側からでも見られる
private static List<Book> _bookList; //static:静的。型につく情報。インスタンス間で共有されるイメージ
//コンストラクタ インスタンス生成時に呼ばれる
public Book(string title){
_title = title;
Book._bookList.Add(this);
}
//静的なコンストラクタも作れる
static Book(){
_bookList = new List<Book>();
}
//メソッド クラスの持つ振る舞い
public string GetTitle(){
return _title;
}
//static,privateなど、こちらも同様に修飾可能
public static Book FindBook(string isbn){
return _bookList.Find(x => x.Isbn == isbn);
}
}
class User
{
static void Main()
{
Book book1 = new Book("リーダブルコード");
book1.Isbn = "9784873115658";
Book book2;
book2 = new Book("シノハユ the dawn of age");
book2.Isbn = "9784757541849";
Book book3 = Book.FindBook("9784757541849");
Console.WriteLine(book3.GetTitle());
}
}
}
シノハユ the dawn of age
クラスから外部にフィールドを公開する際、メソッドを通す。たとえば、GetNameなどのメソッドで名前を取得し、SetNameなどのメソッドで名前に代入したりするコードを書いたことがあるだろう。
C#では、これらの"フィールドを操作するためのメソッド"であるアクセサーを短縮して記述できる。この機能をプロパティという。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
class Book
{
//プロパティを使わないアクセサー
private string _title;
public string GetTitle(){
return _title;
}
public void SetTitle(string title){
_title = title;
}
//プロパティを使うと、等価なコードがこう書ける
public string ISBN { get; set; }
//異なるアクセスレベルでも書ける
public int Price { get; private set;} = 1100;
//ちょっとしたことをしたい場合も書ける
public string ISBN_10{
//注意:上2例と違い、"ISBN_10"のバックフィールドはない
//Get- Set- メソッドの代用
get{
return ISBN.Length==10 ? ISBN : "";
}
set{
ISBN = value.Length==10 ? value : ISBN;
}
}
}
class User
{
static void Main()
{
Book book = new Book();
//使うときは、フィールドと同様
book.ISBN = "9784757551961";
Console.WriteLine(book.ISBN);
}
}
}
9784757551961
C#では、単一継承のみ可能。抽象クラス/メソッドを定義し、オーバーライドすることも可能。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
//基底クラス
class BaseBook{
protected string title;
public BaseBook(string title){ this.title = title; }
public void WriteTitle(){ Console.WriteLine($"Base {title}"); }
}
//基底を継承
class BookIsBase:BaseBook{
//基底のコンストラクタを明示的に呼び出し
public BookIsBase(string title) :base(title){}
//実装を隠蔽する場合、newをつける
public new void WriteTitle(){ Console.WriteLine($"BookIsBase {title}");}
}
//抽象クラス インスタンス生成できないクラス
abstract class AbstractBook{
protected string title;
public void WriteTitle(){ Console.WriteLine($"Abstract {title}"); }
//抽象クラスでは、抽象メソッドを作成可能
public abstract void WriteISBN(string isbn);
}
//抽象クラスを継承
class BookIsAbs:AbstractBook{
public new void WriteTitle(){ Console.WriteLine($"BookIsAbs {title}"); }
//abstractに対して、overrideで実装
public override void WriteISBN(string isbn){ Console.WriteLine($"BookIsAbs {isbn}"); }
}
//抽象クラスを継承2
class BookIsAbs2:AbstractBook{
public new void WriteTitle(){ Console.WriteLine($"BookIsAbs2 {title}"); }
//abstractに対して、overrideで実装
public override void WriteISBN(string isbn){ Console.WriteLine($"BookIsAbs2 {isbn}"); }
}
class User
{
static void Main()
{
BaseBook bb = new BaseBook("Unityで学ぶオンラインゲームのしくみ");
BaseBook b_bib = new BookIsBase("ノンデザイナーズ・デザインブック");
BookIsBase bib = new BookIsBase("独習C#");
AbstractBook ab = new BookIsAbs();
BookIsAbs bia = new BookIsAbs();
AbstractBook ab2 = new BookIsAbs2();
//newしたメソッドは、静的な型のものが呼ばれる
bb.WriteTitle();
b_bib.WriteTitle();
bib.WriteTitle();
ab.WriteTitle();
bia.WriteTitle();
//overrideしたメソッドは、動的な型のものが呼ばれる
ab.WriteISBN("XXXX");
ab2.WriteISBN("YYYY");
}
}
}
Base Unityで学ぶオンラインゲームのしくみ
Base ノンデザイナーズ・デザインブック
BookIsBase 独習C#
Abstract
BookIsAbs
BookIsAbs XXXX
BookIsAbs2 YYYY
インターフェースとは、抽象メソッドのみを持つクラスのようなもの。クラスと違い、多重に継承できる。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
class Book:IISBNGettable,ITitleGettable
{
public string GetISBN(){ return "XXXX"; }
public string GetTitle(){ return "咲日和";}
}
//interfaceの宣言
interface IISBNGettable{ string GetISBN(); }
interface ITitleGettable{ string GetTitle(); }
class User
{
static void Main()
{
Book book = new Book();
IISBNGettable isbnGettable = new Book();
Console.WriteLine(book.GetISBN());
Console.WriteLine(book.GetTitle());
Console.WriteLine(isbnGettable.GetISBN());
}
}
}
XXXX
咲日和
XXXX
列挙型(enum)は、C++/Java同様に扱える。
基本的にintの値が振られるが、char以外の整数型ならどれでも扱える。
マジックナンバーを避けるために用いる。たとえば、遷移先のシーン名。文字列や番号をリテラルで指定してもよいが、タイプミスなど不利益になる場合が多い。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
//"メンバー=数値"の形で書くと、メンバーに相当する値を指定できる
enum Day { Sunday=1, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
//int以外の整数型{byte、sbyte、short、ushort、uint、long、ulong}を使う
enum Month : byte { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
class Hello
{
static void Main()
{
Day day = Day.Sunday;
Console.WriteLine(day.ToString());
Console.WriteLine((int)day);
}
}
}
Sunday
1
構造体。classと違い、値型である。そのほかにも、以下のような制限がある。
- 継承ができない
- 静的な構造体を定義できない
- 引数なしコンストラクタの実装ができない
逆に、たとえばメソッドを定義したり、インターフェースを実装したりすることは可能である。C++(というかC)の直感からは外れるので、注意が必要。
参照型である必要がなく、サイズが小さい場合はクラスより構造体の方がパフォーマンスが上がることがある。
using System;
using System.Collections.Generic;
namespace HelloWorld
{
//struct 構造体の宣言
public struct BookStruct:ITitleGettable
{
//フィールド、メソッドが実装できる 静的なものも可能
private string _title;
public string Title { set{ _title = value;} }
public string ISBN{ get; set;}
private static List<BookStruct> _bookList = new List<BookStruct>();
//コンストラクタは、すべてのフィールドを初期化しなければならない
public BookStruct(string title){ _title = title; ISBN = ""; }
public void PrintTitle(){ Console.WriteLine(_title); }
public string GetTitle(){ return _title; }
}
//構造体とほぼ同等のクラス
public class BookClass:ITitleGettable
{
private string _title;
public string Title { set{ _title = value;} }
public string ISBN{ get; set;}
private static List<BookClass> _bookList = new List<BookClass>();
public BookClass(string title){ _title = title; }
public void PrintTitle(){ Console.WriteLine(_title); }
public string GetTitle(){ return _title; }
}
//クラス、構造体どちらもに実装させてみるインターフェース
public interface ITitleGettable{ string GetTitle(); }
class User
{
static void Main()
{
//構造体の変数宣言&インスタンス生成
BookStruct bookS = new BookStruct("リーダブルコード");
BookClass bookC = new BookClass("オブジェクト指向のこころ");
//変数のコピー
BookStruct bookSc = bookS;
BookClass bookCc = bookC;
//インターフェースの変数にコピー
ITitleGettable bookISc = bookS;
ITitleGettable bookICc = bookC;
bookS.Title = "咲";
bookC.Title = "シノハユ";
Console.WriteLine($"{bookSc.GetTitle()},{bookCc.GetTitle()}");
Console.WriteLine($"{bookISc.GetTitle()},{bookICc.GetTitle()}");
}
}
}
リーダブルコード,シノハユ
リーダブルコード,シノハユ
Unityで使うC#の基礎的な知識を紹介
後半はUnityでよく使う機能を紹介する。
GitHubのリポジトリから、zipファイルをダウンロード&解凍する。
解凍したフォルダにUnityProjectが含まれているので、これをUnityEditorで開く。
開くところまで実演する => 実演相当の資料はこちら
UnityでのC#は、Unity2018で.NET Framework4.6、C#6が使える。
より最新バージョンのC#に追従する方針らしいが、まだ追い付いていないのが現状。
ここまでのC#の基礎については、C#6までの範囲で使えるものを紹介している。
Unityでは、シーンに配置される要素(e.g.カメラ,ライト,3Dモデル,スクリプト)を"GameObject"と呼ばれるオブジェクトに入れて使用する。
このとき、GameObjectに入れる"要素"をコンポーネントと呼ぶ。たとえば、位置を管理するコンポーネントと、当たり判定を行うコンポーネントを取り付ける(アタッチ)と、"当たり判定を持つオブジェクト"が実装できる。
スクリプトをコンポーネントとして扱う場合、継承しなければならないクラスが、MonoBehaviourである。
これを実装したスクリプトをアタッチすると、たとえば、オブジェクトが何かに当った瞬間のようなイベントを受け取ったり、ほかのコンポーネントを参照したりといったことができるようになる。
注意しなければならないのは、MonoBehaviourを継承したクラスでは、コンストラクタによる初期化を行えないこと。これは、アタッチしたスクリプトのインスタンス生成がUnityEditorによって管理されるため。たとえば、UnityのAPIは基本メインスレッドから呼ばなくてはならない。しかし、アタッチしたスクリプトのインスタンス生成は別スレッドで行われるので、それらを書くことができない。また、ゲームを実行していなくても、UnityEditorが裏でインスタンス生成している場合もある。このように、"インスタンス生成時"ではタイミングを取りづらいので、後述のAwakeやStartで初期化を行う。
Unityには、特定の順番で呼ばれるイベント関数が数多く存在する。
よく使うものは、毎フレーム呼び出されるUpdate、最初のフレームのUpdate前に一度だけ呼ばれるStart、オブジェクト生成時に一度だけ呼び出されるAwakeなど。
詳しくは、Unityマニュアル:イベント関数の実行順を参照
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cube : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log($"{gameObject.name}:Start!");
}
// Update is called once per frame
void Update () {
Debug.Log($"{gameObject.name}:Update!");
}
}
C#には、属性(attribute)と呼ばれる、クラスやフィールドに付加情報を与える機能がある。
Unityが用意している属性には、シリアライズ可能であることを示すSerializeField、特定のコンポーネントと一緒にアタッチすることを強制するRequireComponentなどがある。
参考:Unity 属性
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//属性 [属性名]という形で使用する クラスやフィールド、メソッドなどに設定可能
[RequireComponent(typeof(Button))]
public class GenerateButton : MonoBehaviour {
[SerializeField] private GameObject generatedPrefab;
private Button myButton;
// Use this for initialization
void Start () {
myButton = gameObject.GetComponent<Button>();
myButton.onClick.AddListener( () => Instantiate(generatedPrefab) );
}
}
以下の設計に従って、"ババ抜き"のシステムをつくってみましょう。
担当者の作業時間が取れなかったため、中止
追記:実際の勉強会には間に合わなかったものの、完成したので、ここに記す
'ババ抜き'のシステムを作る。
ゲームは、それぞれのプレイヤーの"自分の手札を並べ替える"と"相手の手札を引く"という動作を交互に行うことで進行する。まず引かれる側のプレイヤーが手札をソートし、引く側のプレイヤーがいずれかのカードを引く。このとき、"同じ数字"のカードが2枚そろったら、それらを捨てる。手札が0になったプレイヤーは"あがり"とする。"あがり"でないが一人になるまで、"ソートして引く"を繰り返す。
下のクラス図は、今のババ抜きシステムの"ロジック"にかかわるクラス群。GameSequencerがゲームの流れを制御する。
今回は、"引かれる前のソート"と"どれを引くか"について、具体的な2つの操作を実装せよ。
引く場合、たとえば"ランダムに引く"でもいいし、"一度ババをつかんだところからは絶対に引かない"でもいい。
ソートの場合、たとえば"ババは絶対に右端に置く"とか、"相手がこれまでたくさん引いてるところに置く"でもいい。
本課題で学んでほしいのは、実際にインターフェースを使うと、どのように幸せなのか、また不幸なのかを体感することにある。
書きづらいと思ったなら、それがなぜなのか、どうすれば解消するかを考えることが面白いと私は思う。
C#では、PascalCaseとcamelCaseを使い分ける。
- PascalCase:単語頭を大文字にする
- camelCase:先頭を小文字、それ以降の単語頭を大文字
privateな変数と引数はcamelCase、それ以外はPascalCaseで書くのが一般的。
接頭辞、接尾辞に関する慣例は以下のものがある。
- Interface名には"I"を接頭辞としてつける
- 派生クラスには、基底クラスの名前を接尾辞としてつける
Javadocに相当するものがC#にもある。これはコメント内のタグとして表現される。
Visual Studioを使っている場合、情報を付加したいものの直前の行で"///"と入力すると補完される。
Sandcastleなどのツールを利用すればAPIドキュメントを作成することができる。
/// <summary>
/// MyMethodの要約です。1行で記述します。
/// </summary>
/// <remarks>
/// MyMethodの補足です。
/// 細かい内容はこちらに記述します。
/// </remarks>
/// <param name="x">xの説明</param>
/// <returns>戻り値の説明</returns>
public bool MyMethod(int x)
C#のコーディング規約 .NETのガイド
名前付けのガイドライン .NETのガイド
C# CODING GUIDELINES - Qiita
Unityには、UnityEventというコールバックを扱うクラスが用意されている。
これはメソッドを登録できるクラス。任意のタイミングで登録されたメソッドを呼び出すことができる。
uGUIなど、Unityのもついくつかのコンポーネントは、すでにUnityEventを持っている。それは、たとえばボタンならば"押された時"のようなユーザ入力のタイミングで呼ばれたりする。
自分でインスタンスを持ちたい場合、コルーチンの使用例のように、UnityEventのインスタンスを作成、公開する。例で用いているのは引数0のリスナーを登録する。UnityEventはジェネリックを使ってリスナーに渡す引数を指定できる。"UnityEvent"のような形で宣言でき、Tの値をコールバック実行時に渡す。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// キューブ生成ボタンのコントローラー
/// </summary>
[RequireComponent(typeof(Button))]
public class GenerateButton : MonoBehaviour {
[SerializeField] private GameObject generatedPrefab;
private Button myButton;
// Use this for initialization
void Start () {
myButton = gameObject.GetComponent<Button>(); //コンポーネントの取得
//イベントリスナーの登録
myButton.onClick.AddListener( () => Instantiate(generatedPrefab) );
// "()=> 処理"はラムダ式と呼ばれる記法 デリゲートを記述できる。
}
}
Unityには、Coroutineという非同期処理を書く機能がある。数秒待つ、ロードを待つ、このような数フレームに渡る処理に有効。
コルーチンは"IEnumeratorを返すメソッド"という形で書く。
何かを"待つ"ための中断点に"yield return [HOGE];"を書く。[HOGE]には、"何を待つか?"が入る。Unityはこの"何"に相当する部分をクラスとして持っている。一定秒数待つWaitForSecondsなどがその例である。
作成したコルーチンは、StartCoroutineで開始する。これはMonoBehaviourがもつメソッドで、それぞれのコルーチンはそれぞれのMonoBehaviourインスタンス上で動くことになる。
余談だが、非同期処理にはコルーチンのほかにも、UniRxを使う、async/awaitを使うなど様々な手段が提供されている。Unityでの非同期処理、並列処理なども調べると面白いかもしれない。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// コルーチンで回転させるスクリプト
/// </summary>
public class Rotator : MonoBehaviour {
public UnityEvent OnEndRotation { get; } = new UnityEvent();
private float roteteTime = 10;
private Vector3 firstEulerAngles;
private void Start()
{
firstEulerAngles = transform.eulerAngles;
}
/// <summary>
/// 回転コルーチンを開始するスクリプト
/// </summary>
public void RotationStart()
{
StartCoroutine(RotateCoroutine());
}
//コルーチン
//IEnumratorを返すメソッドなら、StartCoroutineに渡せる
private IEnumerator RotateCoroutine()
{
var startTime = Time.timeSinceLevelLoad;
var diff = Time.timeSinceLevelLoad - startTime;
while (diff <= roteteTime)
{
transform.eulerAngles = firstEulerAngles + new Vector3(360*(diff/roteteTime),0,0);
//ここで中断、1F待つ
yield return null;
diff = Time.timeSinceLevelLoad - startTime;
}
transform.eulerAngles = firstEulerAngles;
OnEndRotation.Invoke();
}
}
Unityマニュアル-コルーチン
Unityスクリプトリファレンス-StartCoroutine
C#には、LINQ(Language Integrated Query)と呼ばれる機能がある。
これは、コレクションの操作を簡潔な記述で行ってくれるものである。例えば、"最大値","条件に合うもの","要素すべてにある処理をした結果群の取得"などができる。
この機能のメリットは、"簡潔に書ける"という部分。forで書いてもいい処理ではあるが、こちらの方が意図が伝わりやすいコードになる。
一方で、適切に使えば、遅延評価の恩恵を受けることもできる。速度面でもforループを行う程度のコストで使用でき、機能の面でも有用。
using System;
using System.Collections.Generic;
using System.Linq;
namespace HelloWorld
{
class Hello
{
static void Main()
{
List<int> list = Enumerable.Range(1, 5).ToList<int>();
foreach(var i in list){
Console.WriteLine(i);
}
//MAX 最大値を返す
Console.WriteLine($"MAX:{list.Max()}");
//Select 射影 ある変換メソッドを通す
foreach(var ii in list.Select(x => x*x)){
Console.WriteLine($"i*i:{ii}");
}
//Where フィルター 条件に合致するものを通す
foreach(var i in list.Where(x => x%2==0)){
Console.WriteLine($"i%2==0:{i}");
}
//メソッドチェーンでクエリを表現できる
var first= list.Where(x => x%2==0).Select(x => x*x*x).First();
Console.WriteLine($"First iii:{first}");
}
}
}
1
2
3
4
5
MAX:5
i*i:1
i*i:4
i*i:9
i*i:16
i*i:25
i%2==0:2
i%2==0:4
First iii:8