コルーチンの使い方
概要
プログラミングの技術のひとつ「コルーチン」に
ついて解説します。
コルーチンを用いたビューとロジックの分離につ
いて解説します。
主にC#を使ったプログラミングを対象としていま
す。
宣伝もあるよ
私は誰なのか
HN:ナムアニクラウド
Twitter: @NumAniCloud
筑波大学AmusementCreators
ゲーム制作サークルNumber-Animal
主にC#プログラミングをする
Twitterアイコン
フリゲ作りました
「冒険者は森に強い」を作りました。遊んでみてね。
コルーチンの使い方
コルーチンとは?
プログラミング言語の機能のひとつ。
関数とかメソッドの仲間
途中で抜けて、後で再開することができる
public IEnumerator Coroutine()
{
Hoge();
yield return null; // ここで一旦抜ける
Hoge(); // あとで実行する
}
定義・実行イメージ
IEnumerator Coroutine(){
Hoge();
yield return null;
Hoge();
Hoge();
yield return null;
yield return null;
}
void Main(){
var c = Coroutine();
c.MoveNext();
Hoge();
c.MoveNext();
c.MoveNext();
c.MoveNext();
}
定義側 利用側
定義・実行イメージ
IEnumerator Coroutine(){
Hoge();
yield return null;
Hoge();
Hoge();
yield return null;
yield return null;
}
void Main(){
var c = Coroutine();
c.MoveNext();
Hoge();
c.MoveNext();
c.MoveNext();
c.MoveNext();
}
定義側 利用側
定義・実行イメージ
IEnumerator Coroutine(){
Hoge();
yield return null;
Hoge();
Hoge();
yield return null;
yield return null;
}
void Main(){
var c = Coroutine();
c.MoveNext();
Hoge();
c.MoveNext();
c.MoveNext();
c.MoveNext();
}
定義側 利用側
定義・実行イメージ
IEnumerator Coroutine(){
Hoge();
yield return null;
Hoge();
Hoge();
yield return null;
yield return null;
}
void Main(){
var c = Coroutine();
c.MoveNext();
Hoge();
c.MoveNext();
c.MoveNext();
c.MoveNext();
}
定義側 利用側
定義・実行イメージ
IEnumerator Coroutine(){
Hoge();
yield return null;
Hoge();
Hoge();
yield return null;
yield return null;
}
void Main(){
var c = Coroutine();
c.MoveNext();
Hoge();
c.MoveNext();
c.MoveNext();
c.MoveNext();
}
定義側 利用側
前提:ゲームループ
更新
描画
多くのゲームプログラムの基本設計
プレイヤー
の更新
敵1の更新 敵2の更新 ……
プレイヤー
の描画
敵1の描画 敵2の描画 ……
1秒間に何度も繰り返す。ループ1回を1フレームという
例
• 下へ移動開始
• 120F待つ
• 5回繰り返す
• 弾を撃つ
• 60F待つ
void Behave(){
下へ移動開始;
120F待つ;
5回繰り返す{
弾を撃つ;
60F待つ;
}
}
書きたいモデルがそのまま書けるのはひとつの理想
だが…
やりたいこと 理想のコード
例 の敵のルーチン
• 下へ移動開始
• 120F待つ
• 5回繰り返す
• 弾を撃つ
• 60F待つ
更新メソッド
ここで処理を止めたいが、そうすると
ゲーム(ゲームループ)全体が止まってし
まう!
void Update(){
下へ移動開始;
120F待つ;
5回繰り返す{
弾を撃つ;
60F待つ;
}
}
ベタな実装
int time = 0;
void Update(){
if(time == 0){
下へ移動開始;
}
if(time => 120 &&
(time - 120) % 60 == 0 &&
time < 120 + 60 * 5){
弾を撃つ;
}
}
毎フレーム呼び出してもらうしかない
ベタな実装
int time = 0;
void Update(){
if(time == 0){
下へ移動開始;
}
if(time => 120 &&
(time - 120) % 60 == 0 &&
time < 120 + 60 * 5){
弾を撃つ;
}
}
毎フレーム呼び出してもらうしかない
時間を数える変数が必要
ベタな実装
int time = 0;
void Update(){
if(time == 0){
下へ移動開始;
}
if(time => 120 &&
(time - 120) % 60 == 0 &&
time < 120 + 60 * 5){
弾を撃つ;
}
}
毎フレーム呼び出してもらうしかない
何をするにも時間を
意識する必要がある
ベタな実装
int time = 0;
void Update(){
if(time == 0){
下へ移動開始;
}
if(time => 120 &&
(time - 120) % 60 == 0 &&
time < 120 + 60 * 5){
弾を撃つ;
}
}
毎フレーム呼び出してもらうしかない
for文が使えないので
剰余で代用
ベタな実装
int time = 0;
void Update(){
if(time == 0){
下へ移動開始;
}
if(time => 120 &&
(time - 120) % 60 == 0 &&
time < 120 + 60 * 5){
弾を撃つ;
}
}
毎フレーム呼び出してもらうしかない
ここを変えると
ここも変える必要あり
コルーチンを使うと
IEnumerator GetFlow(){
下へ移動開始;
for(int y = 0; y < 120; ++y)
yield return null;
for(int i = 0; i < 5; ++i){
弾を撃つ;
for(int y = 0; y < 60; ++y)
yield return null;
}
}
void Update(){
下へ移動開始;
120F待つ;
5回繰り返す{
弾を撃つ;
60F待つ;
}
}
コルーチンを使えば、理想のコードをわりと
そのまま書ける
理想のコード
実行のしかた
// 初期化のとき
flow = GetFlow().GetEnumerator();
// 更新メソッド(毎フレーム呼ばれる)
void Update()
{
flow.MoveNext();
}
C#なら、敵クラスのコンストラクタなどでフロー
の書かれたメソッドを呼び出し、更新メソッドで
MoveNextする
例
RPGの戦闘シーンなどは「流れ」をもつ
1. 作戦入力(入力完了まで待機)
2. スキル発動メッセージ(メッセージ送りまで待機)
3. エフェクト再生(再生終了まで待機)
4. ダメージ表示(表示完了まで待機)
流れを持つ・「待機」だらけ→コルーチンが力を発
揮する
ビューとロジックの
分離
ビューとモデル
多くのプログラムの構造は「ビュー」と「モデル」
に分けられる
ビュー
• UIの表示
• ユーザー入力
• 描画ライブラリを利用した
処理
モデル
• HPなどの値の演算
• データベースへのアクセス
• 処理の流れの制御
• ビューにメッセージを送る
ゲーム
ビューとモデルの分離
プログラム上でもビューとモデルを分けるといいこ
とがある。これが「ビューとモデル(VM)の分離」
ビュー
プロジェクト
モデル
プロジェクト
ゲームエンジン
矢印は依存関係
分離するメリット
ビューでやらないこと、モデルでやらないことが明
確になり、適切にクラス分けなどができる。それに
よって副次的なメリットが発生
ビューでやらないこと
• HPなどの値の演算
• データベースへのアクセス
• 処理の流れの制御
モデルでやらないこと
• UIの更新
• ユーザー入力
• 描画ライブラリの利用
分離するメリット
UIデザインの変更によってビューが変わってもモ
デル部分に影響が及びにくい
使用するゲームエンジンの変更によってビューが
変わってもモデル部分が使いまわせる
サーバーでのチート検証、プランナー向け調整
ツール、テストプロジェクトなどの間でモデルを使
いまわせる
そこに書くべきこと、書くべきでないことが明確
になり、頭のなかが整理される
うちのゲームでも
「冒険者は森に強い」のコードも、ビューとモデル
が分けられているので、その設計を解説します
モデル プロジェクト
ビュー プロジェクト
ゲームでの 分離
ここでは静的なゲームを前提として解説。動的な
ゲームはVMの分離がしにくい
動的なゲーム:HPなどの値が毎フレーム更新され
る可能性があるもの。アクションなどリアルタイム
性のあるゲーム。HPなどをビューに直接持ったほう
がいい場合がある
静的なゲーム:ユーザーの入力があるまで値が更
新されないもの。RPGなどターン制っぽいゲーム
例: の戦闘シーン
ゲームの流れはモデルに、UIの更新はビューに書く
ビュー モデル(流れ)
スキル選択スキル選択待ち
ダメージ発生ダメージ表示
スキル発動発動メッセージ
決定
メッセージ送り
表示完了
ベタな実装
あまり考えたくなかった。
enumで状態を管理して、switchで状態によって分
岐?
新たな処理(たとえば眠り状態メッセージとか)が増えるたび
に、列挙子増やして、switch文の分岐増やして、状態をセッ
トする部分を変更しないといけない
やりたいことに対して書くべきコードが多いし、いろいろな
ところに散らばる
状態が増えるたびに長くなるswitch文…
メソッドとイベントをたくさん作る?
のフロー
モデルはあくまでひとつながりのフロー制御。
ビューの処理を待っているだけ
1. スキル選択(決定を待機)
2. スキル発動(メッセージ送りを待機)
3. ダメージ発生(ダメージ表示完了を待機)
「待機」だらけ→コルーチンが効果を発揮する
モデルをコルーチン化
public Ienumerable<IMessage> GetFlow()
{
var input = new InputSkillMsg(skills);
yield return input;
var skill = input.Response;
yield return new MessageMsg(skill.Message);
yield return new DamageMsg(skill.power);
}
モデル側コード
モデルをコルーチン化
public Ienumerable<IMessage> GetFlow()
{
var input = new InputSkillMsg(skills);
yield return input;
var skill = input.Response;
yield return new MessageMsg(skill.Message);
yield return new DamageMsg(skill.power);
}
モデル側コード
スキル選択を待機
メッセージ送りを待機
ダメージ表示完了を待機
ビューはコールバック
ビュー側コード
private void InputSkill(InputSkillMsg m,
Action<int> callback)
{
this.callback = callback;
}
private void OnDecided(int index)
{
this.callback(index);
}
コールバックを渡される
ので保持
処理が完了したらコール
バックを呼んで通知
モデルとビューの接続
モデルはイテレータブロックでメッセージを投げ
る
ビューはメッセージを受け取って、処理が完了し
たらコールバックを呼ぶ
→ モデルからのメッセージをビューの対応メソッ
ドに渡し、ビューがコールバックを呼んだらモデル
を再開するコードがどこかに必要
チャンネル
Channelクラスという接続用クラスを用意
View
Channel
Model
(フロー)
View
MoveNext
yield
メッセージイベント呼出
コールバック
戻り値 MoveNext
チャンネル
public void AddMessageHandlerTo(Channel channel)
{
// メッセージクラスとメソッドを対応付ける
channel.AddHandler<InputSkillMsg>(InputSkill);
}
private void InputSkill(InputSkillMsg m, …)
{
// スキル選択時の処理
}
Channelにビューのメソッドを登録して使う
ソースを読んでね
更に詳しい実装・仕様は「冒険者は森に強い」の
ソースコードを読んでね!
GitHub:
https://github.com/NumAniCloud/StrongAdve
nturer
落ち穂拾い
コルーチンと戻り値
コルーチンを使っていると、あるコルーチンから別
のコルーチンを呼びたい時がある
ダメージ処理みたいに、いろんな場所で発生する
もの
// コルーチンDamageがあるとして、foreachで呼べる
foreach(var msg in Damage())
{
yield return msg;
}
コルーチンと戻り値
じゃあ、コルーチンを呼んで戻り値が欲しい時は?
// これじゃダメ(コルーチン本体(IEnumerable型)が代入される)
var result = Damage();
// これでもダメ(IEnumerable型にResultなどというものは無い)
var flow = Damage();
foreach(var m in flow)
{
yield return m;
}
var result = flow.Result;
コルーチンと戻り値
コールバックを使う(ちょっと面倒だけど…)
int result = 0;
foreach(var msg in Damage(r => result = r))
{
yield return msg;
}
IEnumerable Damage(Action<int> callback)
{
callback(3);
yield break;
}
定義側
利用側
コールバックを呼ぶと
resultに値が入る
コルーチンが使える言語
C#のイテレータブロック
Pythonのジェネレータ
Rubyのファイバー
Luaのコルーチン
東方弾幕風スクリプトのマイクロスレッド
おしまい でも、宣伝があります
!
「冒険者は森に強い」は、AmusementCreators製
ゲームエンジン「AC-Engine」を使って作られまし
た。
「冒険者は森に強い」のソースコードを公開したの
も、AC-Engineを使った中規模ゲーム制作のサンプ
ルとして参照してもらうため。AC-Engineなら本格
的なゲーム制作のお手本があるのだ!
の特徴
C++, C#対応。Javaなど多言語に対応予
Windows対応。Mac, Linux版も現在調整中!
癖のないアクターシステム。ゲーム制作の面倒事
を省けます
公式サイト(仮)→ http://ac-engine.github.io/
の
ゲーム制作講座執筆中! →
https://github.com/ac-
engine/STGLecture/blob/master/Document/cs
/Index.md
コミッターも募集中! →
https://github.com/ac-engine/ac-engine
利用実績
EXとらぶる冒険記
冒険者は森に強い
利用実績
幻想戦略譚(開発中)
ゆっくり飛行玉
利用実績
Clavis
ColorDomination
本物の終わり お疲れ様でした!

コルーチンの使い方