Rokiのチラ裏

学生による学習のログ

普段使いのコンパイル環境の整備

私は普段ちょっとしたコードに対してコンパイルを行う時、GCC と Clang のどちらかを使う。使うたびにコンパイルオプションを付与するのは面倒なので、これまでは単にオプションを含めた文字列をそのまま .zshrc のエイリアスとして設定していたのだが、あんまり管理の仕方としてはよろしくないなと思ったので、make で管理したいと思った。しかし、毎度 Makefile を作業ディレクトリごとに移動させたりパスを通したり何なりするのは元も子もないので、それに加えて zsh を使ってちょっとした環境整備をした。

で、これを呼び出す zsh スクリプト--version-Sは割と使うのでそれだけ用意した。

そして .zshrc には以下のように指定。これで、gcc, g++, clang, clang++ がこの Makefile の設定で実行される。

alias g++="$HOME/build_setting/make.zsh GXX"
alias clang++="$HOME/build_setting/make.zsh CLANGXX"
alias gcc="$HOME/build_setting/make.zsh GCC"
alias clang="$HOME/build_setting/make.zsh CLANG"

もっと良い管理法などはあるだろうか。個人的にはこれで満足しているので特別問題はないのだが。

Template non-type arguments によるコンパイル時四則演算パーサー

書いて見た。

テスト。

テストの通り、無駄に C++11 に対応している。勿論の事だが C++14 では variable template が使えるし、C++1z(17) では型推論が効くのでそれによって使い勝手も良くなっている。尚、clang 4.0.0 でテストを行ったが、少し前の GCC とかだと

というようなバグに引っかかるので注意。

javascript parseInt 関数の挙動

javascript (ECMAScript 2015) の parseInt 関数の挙動についてメモ。parseInt 関数は、第1引数の文字列をパースし、第2引数に与えられた基数(数学的記数法の底)に基づく整数を返す関数である。基数には、2 ~ 36 までの整数を指定する事ができ、0 を除いたそれ以外の値を指定した場合は NaN を返す*1。以下、ソースから一部改変して引用。

isNaN(parseInt('null',10)) === true // #1
isNaN(parseInt('null',23)) === true // #2
parseInt('null',24) === 23 // #3
parseInt('null',31) === 714695 // #4

♯1 と ♯2の挙動は、分かりやすい。'null'は文字列であり数ではないので、NaNが返ってくる。しかし、♯3 と ♯4 は NaN ではなくそれぞれ両者とも値が返ってくる。これは、第1引数文字列の頭から、第2引数で与えられた基数による単位解釈の基、数として認識できる値が有る限りパースが続き、そうでない部分を切り捨てて返すという parseInt 関数の仕様によるものである。
♯3 では基数として 24 を指定しているが、24進法で n は 23 という値を持つ。しかしその後に続く u は、24進法では値を持たないため無視されて 23 が返ってくる。♯4 は 31進法で全てを有効な数として捉える事ができるため、714695 が返ってくる。
この事実確認のため、MDN web docs の parseInt を見たら、日本語化されたドキュメントが参照している翻訳元が以前のリビジョンでは説明として間違っており、その後更新されて修正されたものの、日本語化されたドキュメントはその間違った説明がされているリビジョンから更新されていなかったため更新を加えた。その時に、parseInt 関数の関係で少し興味深い挙動を示す質問を見つけたので記載する。

stackoverflow.com

parseInt(123123123123123123123123);というように引数には数値が指定されているため、.toStringによって文字列に変換されて以下のようになる。

parseInt("1.2312312312312312e+29").

基数を指定しない場合 parseInt はそれを10進法と解釈するので、"1.2312312312312312e + 29"から + に達してその時点で中断し、"1.2312312312312312"を解析して、1が返される。

Template non-type arguments (Variadic templates) によるコンパイル時乱数生成

Variadic templates でコンパイル時に乱数を生成してみた。現段階ではエンジンとして linear congruential, Xorshift, mersenne twister を、Discrete Uniform Distribution として uniform_int_distribution を実装してみた。ただし Template non-type arguments に対して実数を扱わせる事はできないので*1、そのようなものに対する Discrete Uniform Distribution は linear congruential で用いられる方法を流用している*2また、discard 的なものは未実装であるがこれも追々実装予定。実装した。

github.com

一応こんな感じに書ける。

make_random_sequenceという命名がなんだか微妙だが、make_index_sequenceに則って取り敢えずそうしている。それぞれ乱数エンジンの型は、resultnext_type型とdiscart_type型を持っており、resultが現在の擬似乱数の生成された値、next_typeが次の擬似乱数を生成する型、discart_typeが指定された回数分内部状態が進んだ擬似乱数を生成する型となっている。
尚、パラメータパックの保持する値に対してランダムアクセスを行おうとすると、再帰によって値を1つ1つ捨てる他がないので、これが線形となってしまってとても時間がかかる。これを極力避けた。また、C++1z からテンプレート引数型にautoを記述する事ができるようになったが、これを再帰中に多様すると型推論の負荷が強く、手元のコンパイラ(GCC 7.1.0)では有限時間内にコンパイルが完了せず abort trap で死んだので、 mersenne twister の実装などでは特に型を明示するようにしている。

以下は、srook::anypack<>::random::mersenne_twister のテストコード。実装はこれをパスする。

*1:N4660 [temp.arg.nontype]. ただし参照を除く

*2:std::ratio を使って追々実装するかもしれない

P0707R1 Metaclasses の Design principles

P0707R1 は メタクラスという新しい概念、言語機能についての提案論文である。論文の中身そのものとは少し離れるのだが、1.1 の Design principles が中々良いデザイン原則で他のものにも流用できるな〜と感じたので引用してメモ。

The primary design goal is conceptual integrity [Brooks 1975], which means that the design is coherent and reliably does what the user expects it to do. Conceptual integrity’s major supporting principles are:
  • Be consistent: Don’t make similar things different, including in spelling, behavior, or capability. Don’t make different things appear similar when they have different behavior or capability. – For example, in metaclasses we use normal class declaration syntax instead of inventing novel syntax.
  • Be orthogonal: Avoid arbitrary coupling. Let features be used freely in combination. – For example, in these papers for can be used to process a reflected collection of items (e.g., all the member functions of a class), without having a distinct special-purpose for_each<> on a reflected collection.
  • Be general: Don’t restrict what is inherent. Don’t arbitrarily restrict a complete set of uses. Avoid special cases and partial features. – For example, this paper prefers to avoid creating a special-purpose syntax to declare metaclasses, and instead lets programmers write metaclasses using normal class scope declaration syntax plus the general features of reflection and compile-time programming. Also, metaclasses are just code, that can appear wherever code can appear – written inside namespaces to avoid name collisions (including putting common ones in std::), and shared via #include headers or via modules.
  • These also help satisfy the principles of least surprise and of including only what is essential, and result in features that are additive and so directly minimize concept count (and therefore also redundancy and clutter).

主な設計目標は概念的な完全性である[Brooks 1975]。これは、設計に一貫性があり、ユーザーが期待することを確実に実行することを意味する。概念的完全性の主要な支援原則は

  • 一貫性を保つ:綴り、動作、能力など、似たようなもの同士のそれを変えない。異なる行動や能力を持っているときに、異なるものが似ているように見せない。例えば、メタクラスでは、新しい構文を発明するのではなく、通常のクラス宣言構文を使用する。
  • 直交させる:任意の結合を避ける。機能を自由に組み合わせて使用​​できるようにする。 - 例えば、これらの論文では、リフレクションされたコレクションに対して別個の特別な for_each<>を持たずに、コレクション(例えば、クラスのすべてのメンバー機能)を処理するために使用する事ができる。
  • 一般的にする:固有のものに制限しない。特殊なケースや部分的な機能を避ける。例えば、この論文では、メタクラスを宣言するための特別な構文を作成することを避け、通常のクラススコープ宣言構文とリフレクションとコンパイル時のプログラミングの一般的な機能を使用してプログラマーメタクラスを書き込ませるようにしている。また、メタクラスは名前の衝突を避けるためにstd::名前空間に共通のものを入れたり、#includeヘッダやモジュールを介して共有するなどをしている。

Unity 導入から簡単なスターファイターを作るまでのチュートリアル

学校やその周辺で、Unity が〜…とよく聞くのだが、なんだかんだ触った事がなかったので、先日初めて Unity を触ってみた。そこそこお遊びとしても楽しめたので、わざわざまとめておく事もないかなと思っていたのだが、まあ無駄な事はないだろうと思ったので記事に起こす事にした。一応このエントリを順に読み進めていけば以下のようなものが出来上がる。

対象読者としては、恐らくプログラムを一切書いた事がなくともそこそこできるのではないかと予想している。

インストー

Unity Technologies Japan から導入し、アカウントを作成しておく。アカウントはなくても Unity そのものは動作するが、アセットなどをアセットストアから取り込む事ができないので作っておいた方が楽だ。

プロジェクトの作成

インストールが終わったら Unity を起動する。New から新しいプロジェクトを作る。Project name と Organization は何でも良い。今回は一応 3D ゲームを作るので 3D を選択する。Location もどこでも良いわけだが、一応散らばらないように各々の任意のディレクトリを選択しておく。Create project でプロジェクトを生成する。

f:id:Rok1:20170723152505p:plain

諸々の初期設定

まずここは好みの問題なのだが、簡単のためにまずペインのレイアウトを変更しておく。デフォルトでは以下のようなレイアウトになっている。 f:id:Rok1:20170723153104p:plain これを以下のようにする。 f:id:Rok1:20170723153244p:plain 画像右上の Default となっているボタンをクリックし 2 by 3 に変更する。すると Hierarchy タブと Project タブと Inspector タブが並び、これはこれで使いにくいので Hierarchy の下に Project タブをドラッグして上記の画像のようなレイアウトにした。次に、Project タブ内を見ると、本プロジェクトのディレクトリ階層が表示されているので(まだ Assets しかないはずだが)その Assets の中に自分がこれから記述するコードを入れるディレクトリを作っておく。ここで階層を分けておくのは、後々アセットを外部から導入した時にそれらをごちゃ混ぜにならないようにするためだ。ディレクトリの作成は、Assets を右クリックし Create から Folder を選択する。ディレクトリ名は便宜上 MyDir とした。さらに Mydir の内部に src と prefab というディレクトリを作成しておく。これらの作業を終えると以下のようになっているはずだ。

f:id:Rok1:20170723154520p:plain:w240

次に本プロジェクトで使う 3D モデルをアセットストアから導入する。3D モデルは Maya などで自作する事もできるが、動かしたいだけならアセットストアから導入してしまった方が楽だ。アセットストアは Window から Asset Store で開く事ができる。今回はアセットストアから背景モデルと戦闘機二機を入手する。モデルは何でも良いのだが、便宜上背景モデルには 3 Skyboxes 2 、自機には Free SciFi Fighter、敵機には Space fighter を使う。それぞれ、アセットストア上部の Search からそのまま検索すれば出てくるはずだ。Download をクリックして完了すると Import するかのウィンドウが出てくるので Import をクリックする。全ての作業を終えると以下のようになっているはずだ。

f:id:Rok1:20170723155828p:plain:w240

ゲームシーンへの追加

次に、入手したアセットをゲームシーンへ追加する。追加は、モデルデータを Hierarchy タブにドラックアンドドロップする事で可能だ。まずは、自機と背景モデルと読み込む。Free SciFi Fighter の場合、モデルデータは /Assets/Meshes にあるのでこれをそのまま Hierarchy タブにドラックアンドドロップする。

f:id:Rok1:20170723171648p:plain 次に背景モデルを読み込む。背景モデルは Hierarchy の Main Camera から設定する。Main Camera を選択したら Inspector の Add Component で Skybox と検索し選択する。Inspector に Skybox が追加されているはずだ。さらに、Skybox の歯車のマークの下部にある円とその中心に点が描かれているボタンをクリックする。すると、Select Material ダイアログが表示されるので、任意の背景を選択する。選択に応じて、Game タブの背景も変わるはずだ。こちらでは、便宜上 BlueGreenNebular を選択してみた。この段階では、SciFi Fighter が目の前に来ていてとても見にくいので、カメラの位置を変えてやる。カメラの Inspector タブの Transform から任意にカメラの位置を変更する事ができる。取り敢えず、Position のY軸を 10、Z軸を -35 とした。これまでの作業を終えると以下のようになっているはずだ。

f:id:Rok1:20170723172637p:plain ここいらで一度保存しておこう。/Assets/MyDir/ 配下に先ほどと同じように今度は scene というディレクトリを作っておく。Windows なら Ctrl + s で、Mac なら ⌘ + s でシーンの保存ができる。これを行うと保存先を指定できるので今作成した /Assets/MyDir/scene ディレクトリ内部に該当シーンを保存する。シーン名は、便宜上 game_scene とした。保存が完了すると Hierarchy タブ内の該当シーンが、保存したシーン名になっているはずだ。

シューティングゲーム本体の作成

シューティングゲームそのものを先に作るか、メニュー画面などを先に作るかはその方針によって様々になるだろうが、取り敢えずまずはシューティング要素そのものを作る事とした。で、まずは自機を制御するコードを書く。Unity では言語としては C#, javascript, Boo(Python) という選択指があるが C# の利用を勧めるユーザーが多いようなので C# を利用する。Unity を利用するにあたってどの言語を利用するかは最終的にはその状況や過去の経験に依存する事と思うのでその辺りはよく考えた方が良いだろう。

ファイルの作成とエディタの設定

ソースコードの作成もディレクトリ作成の操作と似たような感じで右クリックから Create 、C# Script というように選択する。これは、/Assets/MyDir/src/ 内に作成する。ファイル名は starfighter.cs とした。

f:id:Rok1:20170723175214p:plain:w240

早速コードを書きたいところだがその前に、Unity は任意のエディターでコードを編集する事ができる。デフォルトでは monodevelop を使うようになっている。これで良ければそのままで問題ないのだが、筆者は普段 vim を使っておりどうせならば vim で編集したいところなので、そのように設定した。なのでそのようにしたくない場合、この部分は飛ばして問題ない。
さて Mac の場合、普通にターミナルエミュレータなどで vim を使っているのであれば大抵それは普通の vim であり macvim ではないと思うのだが、筆者は macvim を持っていなかったのでそれを導入した。特別日本語を使うとも思わないが、もしかしたら今後そういう事もあるかもしれないと思ったので取り敢えず以下のものを入れた。導入方法はリンク先にある通りだ。


導入したら、Preferences の External Toolsから、macvim を設定する。しかし単純に macvim の実行ファイルを指定するだけだと Unity から呼び出す度に新しいウィンドウが生成されてしまうのでこれはよくない。そこで、macvim を以下のように設定する。

f:id:Rok1:20170723180230p:plain:w300

さらに当然 vim に補完させたいので、 GitHub - kitao/unity_dict: Generates the keyword list of Unity for Vim を利用して vim に辞書を登録しておく。

自機を動かす

先ほど作成した starfighter.cs というファイルを Project タブの /Assets/MyDir/src/ から開くと以下のようなコードが既に構成されているはずだ。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        
    }
}

Start 関数はこのオブジェクトが構築された時、一番初めに一度だけ呼ばれる関数であり、いわばコンストラクタのように動く。Update 関数は毎フレームごとに呼ばれるメインループの関数だ。見てわかる通り至って単純であり、ゲーム内のオブジェクト間の処理や諸々の設定は全て GUI や自動設定によって勝手に Unity が済ましてくれるため殆どコードを書く必要がないのだ。
取り敢えず、まずはキーボードの入力に従って自機が動くようにしよう。入力は矢印キーから行うことにしよう。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();

    // Use this for initialization
    void Start () {
        
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    // Update is called once per frame
    void Update () {
        get_key();
    }
}


Input.GetAxis("Vertical")によって上下キーの入力に応じて -1~1 の値を取得、Input.GetAxis("Horizontal")によって左右キーの入力に応じて -1~1 の値を取得する事が出来る。if 文を else if とすると斜め移動を禁止できるのでそこらへんは好みで良い。transform.Translate 関数によって、自身の方向と距離を定める事ができる。保存したら、Hierarchy 内の SciFi_Fighter_AK5 に starfighter ファイルをドラックアンドドロップする。SciFi_Fighter_AK5 を Hierarchy から選択し Inspector タブを確認する。Starfighter(Script) が追加されていれば今書いたコードがそのゲームオブジェクトと紐づいたことを示す。これで、もう自機が上下左右のキーボード入力によって移動するようになったはずだ。上部の ▶️ ボタンをクリックすると Game タブでシミュレートが行える。コード中にエラー箇所があれば下部にその旨を示すエラーメッセージが行番号付きで示されているはずなのでそれを確認して対応する。

さて、自機の移動ができるようになったのは良いが、このままだと自機が永遠に画面外へ移動できてしまう。移動範囲を画面内に納めるよう制御を加えよう。まずは x 軸を設定する。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public const float max_x = 35.0f;

    // Use this for initialization
    void Start () {
        
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
    }
}

max_x が x 軸の最大の値だ。publicにしているのは、Inspector タブからこの値を任意に変更できるようにするためだ。SciFi_Fighter_AK5 を Hierarchy タブから選択して Starfighter(Script) の Max_x を弄ると自機の移動可能な領域を変えられるので任意に設定すると良いだろう。次に、Y 軸に対する設定だ。Y 軸に対しても同じように移動領域に制限を設けるのも良いが、今回は領域は設けず、代わりにカメラを自機について行かせるようにしよう。
/Assets/MyDir/src 配下に camera_control.cs を作成し以下のように記述する。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class camera_control : MonoBehaviour {

    Vector3 diff;
    float diff_x;

    // Use this for initialization
    void Start () {
        diff = transform.localPosition;
        diff_x = diff.x;
    }
    
    // Update is called once per frame
    void Update () {
        if(GameObject.Find("SciFi_Fighter_AK5")){
            Vector3 pre = GameObject.Find("SciFi_Fighter_AK5").transform.localPosition;
            transform.localPosition = new Vector3(diff_x,pre.y + diff.y,pre.z + diff.z);
        }
    }
}

シーン中の ”SciFi_Fighter_AK5″ というオブジョクトの位置を取得し、それに対してそのオブジェクトの最初の位置を足した値を現在のカメラの位置として設定している。これを、Hierarchy の Main Camera にドラックアンドドロップをして、コードと紐づける。
以上で、自機が画面から移動しすぎて消えてしまったり前に行きすぎて小さくなってしまったりといった事はなくなったはずだ。▶️ ボタンをクリックして確認できる。

弾を出す

次に弾を出していく。弾の 3D モデルはまたもやアセットストアから利用してしまおう。取り敢えず、Simple Particle Pack というものを使うことにした。アセットストアからダウンロードしインポートしたら /Assets/SimpleParticle/Resources/Directional/ から任意の *.prefab を選択して Hierarchy にドラックアンドドロップする。便宜上、取り敢えず Torch(Blue) を入れた。このままだと SciFi_Fighter_AK5 の上に今 Hierarchy に移した Torch(Blue) が重なってしまい様子がよく見えないと思うので、一旦 SciFi_Fighter_AK5 を非表示にしよう。SciFi_Fighter_AK5 を選択して Inspector タブの左側にあるチェックを外すと非表示にする事ができる。さて、ここまでの作業が完了すると以下のようになっているはずだ。

f:id:Rok1:20170723190342p:plain

さて、この弾に対して制御を与えていく。/Assets/MyDir/src 配下に bullet というファイル名で C# ファイルを作成し、以下の内容を記述する。

// bullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class bullet : MonoBehaviour {

    float speed = 3;

    // Use this for initialization
    void Start () {
        Destroy(this.gameObject,5);
    }
    
    // Update is called once per frame
    void Update () {
        transform.Translate(0,0,speed);
    }
}

Destroy 関数によって対象オブジェクトを破棄する事ができる。これは、5 と指定しているので Start 関数が実行された 5秒後に自身を破棄するようにしている。また、弾は打つと前方に向かっていく動きをするはずだから、常に前方に向かうように設定している。
このソースファイルを先ほどと同じように、今度は Torch(Blue) にドラッグアンドドロップをし紐づける。

次にこの Torch(Blue) をプレハブとして作成しておく。プレハブとは…

適切な値のコンポーネントを追加した個々のゲームオブジェクトでシーンを構築するのは簡便ですが、シーン内に繰り返し使用されるNPCや小道具、背景パーツなどのオブジェクトが有る場合には、問題点もあります。単純にオブジェクトをコピーして複製すると、それらのプロパティ全てが独立したものになってしまいます。一般的には、ある特定オブジェクトのインスタンス全てのプロパティを揃えた い事が多いので、シーン内でオブジェクトを編集したら、その編集をコピー全てに適用したい、と考えるでしょう。 幸いなことに、Unity は、GameObject オブジェクトのコンポーネントとプロパティーすべてを格納する Prefab アセットタイプを持っています。プレハブはテンプレートとして使われ、シーン内に新しいオブジェクトインスタンスを作成することができます。プレハブアセットに加えられたすべての編集内容はすぐに生成されたすべてのインスタンスに反映されますが、各インスタンス別にコンポーネン トを オーバーライド したり、設定したりできます。 https://docs.unity3d.com/ja/current/Manual/Prefabs.html 引用

プレハブ化は簡単で、先ほど Hierarchy に追加した Torch(Blue) を /Assets/MyDir/prefab 配下にドラッグアンドドロップする事で Torch(Blue) のプレハブ化が完了する。尚、今 Hierarchy 上にある Torch(Blue) は使わないので削除しておく。わかりやすいよう、今作られた Torch(Blue).prefab を my_bullet.prefab に改名しておく。ここまでの作業が完了すると以下のようになっているはずだ。f:id:Rok1:20170723193152p:plain

次に、今度はスペースキーを押すと、自機がこの弾を射出するようにしよう。/Assets/MyDir/src/starfighter.cs を編集する。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public float interval = 0;
    public float max_x = 35.0f;

    // Use this for initialization
    void Start () {
        
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
    }
}

追加したのは、shoot関数とその内部で使うGameObjectintervalだ。GameObjectはプレハブを読み込む事のできる型でこれを public とする事で GUI から任意のプレハブを設定できるようにしている。intervalは、スペースを連続的にまたは継続的に押し続けた場合に、弾をどれだけ射出するかを決定する、その名の通り弾の射出のインターバルを定める変数だが、これも public とする事で Inspector タブから弾の射出度を任意に打ち込めるので調節が楽だ。
編集を終えたら、Hierarcy から SciFi_Fighter_AK5 を選択し、Inspector の Starfighter(Script) を確認する。そこに Bullet と Interval という項目が追加されているはずなので、Bullet に /Assets/MyDir/prefab/my_bullet.prefab をドラックアンドドロップする。Interval は 0 となっていると思うが、その値は先ほども述べた通り弾の射出頻度を設定するので、任意に変えて調節すると良いかもしれない。ここまでの作業が完了すると、以下のようになっているはずだ。

f:id:Rok1:20170723202900p:plain

さて、これでもう弾を射出できるようになっているので、▶️ボタンをクリックして試してみよう。

敵機を出す

まずは敵機を Hierarchy に読み込む必要がある。/Assets/SpaceFighter/Art Assets/FighterMedeum_FBX.fbx を Hierarchy タブへドラッグアンドドロップする。取り敢えず、自機に重なって見えないので適当に Inspector からPosition, Rotation, Scaleを弄って適切な大きさに設定する。

f:id:Rok1:20170723204048p:plain

画像にもある通り、敵機は自機と対面してくるはずなので、Rotation の Y軸は 180 になるはずだ。ではこの敵機に対して処理を書いていく。/Assets/MyDir/src 配下に enemy.cs を作成し記述する。

// enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy : MonoBehaviour {

    public GameObject bullet;
    float z_speed = 0.7f;
    public float interval = 0;

    // Use this for initialization
    void Start () {
        
    }

    void shoot(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        interval += Time.deltaTime;
        if(interval >= 0.3f){
            interval = 0.0f;
            Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),quat);
        }
    }
    
    // Update is called once per frame
    void Update () {
        transform.Translate(0,0,z_speed);
        shoot();
    }
}

敵機は何もしなくとも自動でこちらに迫ってきてほしいので、そのように設定している。これを Hierarchy の FighterMideum_FBX にドラックアンドドロップしてコードを紐づける。そして、敵の弾は自機と違うものにしたいので、/Assets/SimpleParticlePack/Resources/Directional から任意のプレハブを Hierarchy にドラックアンドドロップする。今回は便宜上、Torch(Green) を選んだ。次に Torch(Green) を Hierarchy から選択し、Transform の Rotation の Y軸を 180度回転させておく。これは、弾が敵側からこちら側に向かってくるはずだからだ。その後、/Assets/MyDir/src/bullet をこの Torch(Green) にドラッグアンドドロップしてコードを紐づける。さらに、この Torch(Green) を Hierarchy から /Assets/MyDir/prefab にドラッグアンドドロップしてそれを enemy_bullet.prefab へと改名し、Hierarchy の enemy_bullet を削除する。ここまでの作業が完了すると以下のようになっているはずだ。

f:id:Rok1:20170723205558p:plain

そして、FighterMedium_FBX を Hierarchy から選択し、Enemy(Script) の Bullet に /Assets/MyDir/prefab/enemy_bullet.prefab を ドラッグアンドドロップして紐づける。これで敵が進行しながら弾を発車するようになったはずだ。次に、敵の出現位置を設定していく。敵の出現位置は、自機の前方のうちランダムな x 軸上(画面内)に一定間隔で出現すれば良いだろう。この処理は、本当は敵機のコード上で管理した方が後々に機能を追加する事を考えると適切なのだが、取り敢えず簡単に自機の座標を使うために、starfighter.cs にこの処理を記述する。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public GameObject enemy;
    public float interval = 0;
    public float max_x = 35.0f;
    public float enemy_interval = 4.0f;
    float enemy_time = 0;
    public float enemy_distance = 200;

    // Use this for initialization
    void Start () {
        
    }

    void get_enemy(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        enemy_time += Time.deltaTime;
        if(enemy_time >= enemy_interval){
            enemy_time = 0.0f;
            Instantiate(enemy,new Vector3(Random.Range(-max_x,max_x),transform.position.y,transform.position.z + enemy_distance),quat);
        }
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
        get_enemy();
    }
}

追加したのは、get_enemy 関数だ。実機 + enemy_distance の距離でenemy_timeに設定した秒数で出現させる。画面内の x 軸上に、ランダムに出現させるために、Random.Range関数を用いている*1。次に FighterMedium_FBX を /Assets/MyDir/prefab へとドラッグアンドドロップし、プレハブ化する。Hierarchy 上の FighterMedium_FBX はもう使わないので削除する。ここまでの作業が完了すると以下のようになっているはずだ。 f:id:Rok1:20170723215727p:plain

この段階で、実機の前方に敵機がランダムに発生するはずだ。▶️ で確認できる。

衝突判定をつける

まずは、こちらの撃った弾が敵機に当たるようにする。/Assets/MyDir/prefab/my_bullet.prefab を Hierarchy タブにドラッグアンドドロップする。Hierarchy に移した my_bullet を選択して、Inspector の Add Component で Box Collider を選択する。これが衝突判定を設定するコンポーネントとなる。まず Is Trigger にチェックを入れる。次に Hierarchy から SciFi_Fighter_AK5 を選択して一旦非表示にしてから Box collider の Center や Size を弄って任意に衝突範囲を設定する。

f:id:Rok1:20170723221356p:plain

設定が完了したら、Inspector タブ上部の右側にある Apply ボタンを押して適用し、Hierarchy タブから my_bullet を削除する。続けて、/Assets/MyDir/prefab/FighterMedium_FBX.prefab を Hierarchy タブにドラッグアンドドロップし、Inspector タブの Add Component から Box Collider と Rigidbody を追加する。Box Collider は同じく Is Trigger のチェックをしてから衝突判定の範囲を定める。Rigidbody は Unity の物理エンジンの管理下に置くものであるが、今回重力は用いないので Use Gravity のチェックを外しておく。

f:id:Rok1:20170723222603p:plain

設定が完了したら、同じく Apply をクリックし、Hierarchy から FighterMedium_FBX を削除する。次に、弾が当たった場合に機体を爆発させたいので爆発のエフェクトを作成する。これは、/Assets/SimpleParticlePack/Resources/Explosions 内にあるので任意のエフェクトを選ぼう。便宜上、Explosion01c を選んだ。これを、Hierarchy にドラッグアンドドロップし、/Assets/MyDir/prefab にドラッグアンドドロップしておく。これで、爆発エフェクトの完成だ。Hierarchy 上のものはもう使わないので Explosion01c を削除する。さて、先ほどの設定で弾と敵があたった時の衝突判定が取得できるようになったので、/Assets/MyDir/src/enemy.cs を編集して衝突があった場合の処理を書こう。

// enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class enemy : MonoBehaviour {

    public GameObject bullet;
    public GameObject explosion;
    float z_speed = 0.7f;
    public float interval = 0;

    // Use this for initialization
    void Start () {
        
    }

    void shoot(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        interval += Time.deltaTime;
        if(interval >= 0.3f){
            interval = 0.0f;
            Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),quat);
        }
    }
    
    // Update is called once per frame
    void Update () {
        transform.Translate(0,0,z_speed);
        shoot();
    }

    void OnTriggerEnter(Collider coll){
        if(coll.gameObject.tag == "Player_Bullet"){
            Instantiate(explosion,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            Destroy(this.gameObject);
        }
    }
}

OnTriggerEnterに衝突判定があった場合の処理を記述する。coll.gameObject.tagとはこれから弾に設定するユニークなタグであり、これをプレイヤーの弾であった場合に関する分岐に利用する。explosionオブジェクトを新たに生成しているが、これは爆発のエフェクトを GameObject に差し込む事を前提としている。その後、Destroy関数によって自身を破棄する。
あとは、タグの設定と、爆発オブジェクトを設定するだけだ。/Assets/MyDir/prefab/my_bullet.prefab を Hierarchy にドラッグアンドドロップして、Inspector から Tag を設定する。Add Tag から “Player_Bullet” というタグを新規に作成し、my_bullet に適用し Apply をクリックする。

f:id:Rok1:20170723225350p:plain:w250

そして Hierarchy から my_bullet を削除する。次に、/Assets/MyDir/prefab/FighterMedium_FBX.prefab を Hierarchy にドラッグアンドドロップし、 Inspector から Enemy(Script) の Explosion に /Assets/MyDir/prefab/Explosion01c.prefab をドラッグアンドドロップする。これで、こちらの射出した弾が敵に当たると、爆発して敵が消滅するようになったはずだ。

自機に弾が当たるようにする

後は自機に弾が当たればゲーム部分はひとまず完成だ。まずは /Assets/MyDir/src/starfighter.cs を編集して衝突判定のコードを先に書いてしまう。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public GameObject enemy;
    public GameObject explosion;
    public float interval = 0;
    public float max_x = 35.0f;
    public float enemy_interval = 4.0f;
    float enemy_time = 0;
    public float enemy_distance = 200;

    // Use this for initialization
    void Start () {
        
    }

    void get_enemy(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        enemy_time += Time.deltaTime;
        if(enemy_time >= enemy_interval){
            enemy_time = 0.0f;
            Instantiate(enemy,new Vector3(Random.Range(-max_x,max_x),transform.position.y,transform.position.z + enemy_distance),quat);
        }
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
        get_enemy();
    }

    void OnTriggerEnter(Collider coll){
        if(coll.gameObject.tag == "Enemy_Bullet"){
            Instantiate(explosion,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            Destroy(this.gameObject);
        }
    }
}

続いて Hierarchy の SciFi_Fighter_AK5 を選択して Inspector から Explosion に /Assets/MyDir/prefab/Explosion01c.prefab を設定する。続けて /Assets/MyDir/prefab/enemy_bullet.prefab を Hierarchy にドラッグアンドドロップして、先ほどと同じように、これに対してタグをつける。タグ名は、"enemy_bullet" だ。

f:id:Rok1:20170723231249p:plain:w250

Apply をクリックして Hierarchy から削除する。そして、SciFi_Fighter_AK5 の Inspector で Rigidbody と Mesh Collider を設定して下図のようにする。

f:id:Rok1:20170723232228p:plain:w250

最後に、/Assets/MyDir/prefab/enemy_bullet を Hierarchy にドラッグアンドドロップして Box Collider をコンポーネントに追加して Apply 、Hierarchy から削除する。これで、自機と敵機両方とも、射出した弾に当たれば爆発して消滅するようになったはずだ。

メニュー画面の作成

ゲーム本体部分は完成したので、次にメニュー画面を作成する。Hierarchy タブ上で右クリックし、UI から Button をクリックする。続いて Scene タブの 2D ボタンをクリックし Hierarchy から今作成した Button をダブルクリックすると、作成した Button が確認できる。Button から Text を選択し Inspector からボタンの内容を変更できる。

f:id:Rok1:20170723233606p:plain

ここから、ボタンを押した時の処理を加えていく。/Assets/MyDir/src/ に game_control.cs を作成し以下のように記述する。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class game_control : MonoBehaviour {

    public GameObject Start_btn;
    public GameObject player;
    public GameObject Reset_btn;
    bool game_status = true;
    
    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        if(!game_status){
            Reset_btn.SetActive(true);
        }
    }

    public void StartButton(){
        Instantiate(player);
        Start_btn.SetActive(false);
    }

    public void ResetButton(){
        Application.LoadLevel("game_scene");
    }
}

スタートボタンが押された時に自機を生成するようにする事で、ゲームの開始を行うようにしてある。また、game_statusをパブリックに持たせて、この状態に応じてリセットボタンを出すようにする。つまり、このフラグにゲームオーバーの状態を外部から知らせる必要がある。これを Hierarchy の Main Camera にドラッグアンドドロップをして紐づける。次に、Hierarchy から Button を選択、Inspector 下部の On Click()を下図のように設定する。

f:id:Rok1:20170723235133p:plain:w250

これで、このボタンが押された時に、StartButton 関数が呼ばれるようになる。次に、Hierarchy にある SciFi_Fighter_AK5 を /Assets/MyDir/prefab にドラッグアンドドロップしてプレハブ化し、Hierarchy から削除する。尚、今までは、 Hierarchy タブの中にあった自機をカメラで追う設定だったが、ボタンを押してから生成される自機に対して追尾するようにする必要があるため、/Assets/MyDir/src/camera_control.cs にて追尾するオブジェクトの名前を変更してやる必要がある。

// camera_control.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class camera_control : MonoBehaviour {

    Vector3 diff;
    float diff_x;

    // Use this for initialization
    void Start () {
        diff = transform.localPosition;
        diff_x = diff.x;
    }
    
    // Update is called once per frame
    void Update () {
        if(GameObject.Find("SciFi_Fighter_AK5(Clone)")){
            Vector3 pre = GameObject.Find("SciFi_Fighter_AK5(Clone)").transform.localPosition;
            transform.localPosition = new Vector3(diff_x,pre.y + diff.y,pre.z + diff.z);
        }
    }
}

次に、リセットボタンを作成する。スタートボタンと同じく作成する。

f:id:Rok1:20170724000403p:plain:w250

さらに、/Assets/MyDir/src/starfighter.cs 内でゲームオーバーの判定を行うために、game_control の game_status を反転させる。

// starfighter.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class starfighter : MonoBehaviour {

    public class Speed{
        public float x = 2;
        public float z = 2;
    }

    Speed speed = new Speed();
    
    public GameObject bullet;
    public GameObject enemy;
    public GameObject explosion;
    public float interval = 0;
    public float max_x = 35.0f;
    public float enemy_interval = 4.0f;
    float enemy_time = 0;
    public float enemy_distance = 200;

    // Use this for initialization
    void Start () {
        
    }

    void get_enemy(){
        Quaternion quat = Quaternion.Euler(0,180,0);
        enemy_time += Time.deltaTime;
        if(enemy_time >= enemy_interval){
            enemy_time = 0.0f;
            Instantiate(enemy,new Vector3(Random.Range(-max_x,max_x),transform.position.y,transform.position.z + enemy_distance),quat);
        }
    }
    
    void get_key(){
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");

        if(Input.GetKey("up")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("down")){
            transform.Translate(0,0,vertical * speed.z);
        }
        if(Input.GetKey("left")){
            transform.Translate(horizontal * speed.x,0,0);
        }
        if(Input.GetKey("right")){
            transform.Translate(horizontal * speed.x,0,0);
        }
    }

    void check_position(){
        Vector3 pos = transform.position;
        pos.x = Mathf.Clamp(transform.position.x,-max_x,max_x);
        transform.position = pos;
    }

    void shoot(){
        interval += Time.deltaTime;
        if(Input.GetKey("space")){
            if(interval >= 0.1f){
                interval = 0.0f;
                Instantiate(bullet,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            }
        }
    }


    // Update is called once per frame
    void Update () {
        get_key();
        check_position();
        shoot();
        get_enemy();
    }

    void OnTriggerEnter(Collider coll){
        if(coll.gameObject.tag == "Enemy_Bullet"){
            Instantiate(explosion,new Vector3(transform.position.x,transform.position.y,transform.position.z),Quaternion.identity);
            Destroy(this.gameObject);
            GameObject.Find("Main Camera").GetComponent<game_control>().game_status = false; // 反転
        }
    }
}

最後に、Main Camera の Inspector から、Game_control(Script) に、各ゲームオブジェクトを設定する。Start_btn には Hierarchy のStartButton を Player には /Assets/MyDir/prefab/SciFi_Fighter_AK5.prefab を Reset_btn には Hierarchy の ResetButton を設定し、ResetButton を非表示にする。

f:id:Rok1:20170724003604p:plain:w250

これで、スタートボタンから始まり、ゲームオーバーするとリセットボタンが表示されるようになった。これで一通り完成である。

ゲームのビルド

最後にビルドであるが、File から Build Settings で様々なプラットフォーム用に出力設定が行える。取り敢えず今回は PC,Max and Linux Stand Arone に設定する。Build and Run として以下のようにプレイできる。

f:id:Rok1:20170724004840g:plain

ちょっとスピードが全体的に早すぎる気もするが、まぁそこらへんはパラメータを変えれば程よい感じになるだろう。

総括

という感じで、Unity は、このような簡単なゲームであればあまりコードを書かずに、殆どを GUI の操作で済ませる事ができる。しかし、逆に多くをラップされている分、内部で何が起こっているのかはしっかりとドキュメントなど読んで把握しなければならないが、そこそこ把握できれば恐らくなんでも作れるだろう。このチュートリアルが何かの役に経てば光栄である。

個人的には、自作でゲームエンジンを作ってみたくなった。

*1:乱数生成アルゴリズムについての記述が公式ドキュメントにはないが、一応プラットフォーム固有のランダムジェネレータを用いているようだ。気に入らなければ同リンクから fast PRNG を利用できる。

モナドの概念をC++に導入して冗長なエラーハンドリングを回避する

モナドの概念をC++に導入する事についての Jonathan Boccara 氏による投稿のシリーズを見て、興味深く感じたので、個人的なメモ。尚ソースからは若干コードなどが改変されているところがある。

さて、この具体的な方法は、新たに関数を作成する場合と、既存の関数に対する対応の2種類に分ける事ができるので、それらを区別して記載する。

新たに関数を作成する

次に4つの関数がある。

int f1(int a);
int f2(int b, int c);
int f3(int d);
int f4(int e);

これらを、次のような順番で呼び出したい。

  • f1 を二度呼び出す
  • f1 の結果を f2 の引数にして呼び出す
  • f2 の結果を f3 の引数にして呼び出す
  • f3 の結果を f4 の引数にして呼び出す


しかし、それぞれが失敗するかもしれない。エラーハンドリングの方法を考える必要がある。

  • bool 型などでエラーまたは成功ステータスを返却する、C言語スタイルの解法。
    → 関数から戻ってくる bool 型はエラーまたは成功を意味する保証はない。また結果を全て照合しなければならないので、大量のif文のネスト、または &= などによる判定が必要。エラー内容を通知する事ができない。
  • assert する
    → 処理を単に止めたければ良いかもしれないが、そうでない場合は不十分。エラー内容(エラー箇所)の通知が可能。
  • 例外を投げる
    → パフォーマンス面、例外安全性の考慮が必要。エラー内容の通知が可能。
  • Optional 型を使う
    → 関数から戻ってくる値は必ずエラーまたは成功を内包する。例外送出時のようなパフォーマンス面、例外安全性を考慮する必要はない。依存する関数の数に応じて線形に if文のネストが増える。value_or などで独自的に決められたエラーコードと照合すれば(例えば &= とかで) if文のネストは無くても良いかもしれないが、独自的に決められた値を用いた瞬間から Optional 型の意味は成さない。エラー内容を通知する事が出来ない。


実行時エラーハンドリングの方法としてはこのようなものが考えられるが、Optional + モナドC++で用いる事で、単純で安全なエラーハンドリングが可能になる。具体的には受け取った値が有効かチェックして、有効であれば関数を呼び出し、そうでなければ無効値を返し続ける関数を実装すれば良い。この関数はとても簡単に実装できるが、プロトタイプに対して2つの考慮が必要である。

  • 関数ではなく演算子とする。これによって呼び出しの連鎖が行われる時、より直感的な構文を作る事ができる。モナドがより多く使われる[要出典] Haskell において>>=が使われている事から、>>=を使う。
  • 関数は任意の呼び出し可能な型(関数、関数ポインタ、std::function、lambdaまたは関数オブジェクト)と互換性がなければならない。


実際の実装はこんな感じだろうか。

template<class T,class Invocable,std::enable_if_t<std::is_invocable_v<std::decay_t<Invocable>,T>,std::nullptr_t> = nullptr>
auto operator>>=(const std::optional<T>& t,Invocable&& invoke) noexcept(noexcept(invoke(*t))) -> decltype(invoke(*t))
{
    if(t){
        return invoke(*t);
    }else{
        return std::nullopt;
    }
}

尚 f1 ~ f4 の戻り値は、エラーまたは成功ステータスを表す Optional 型にする。

std::optional<int> f1(int a);
std::optional<int> f2(int b, int c);
std::optional<int> f3(int d);
std::optional<int> f4(int e);

これを使って、冒頭の関数らを適用させると、以下のように書ける。result には途中の関数のいずれか1つでも失敗すれば無効値が入る。

std::optional<int> result = 
    f1(x) >>= [=](int b){
        return f1(y) >>= [=](int c){
            return f2(b,c) >>= [=](int d){
                return f3(d) >>= [=](int e){
                    return f4(e);
                };
            };
        };
    };

既存の関数に対応する

次に、f1 ~ f4 を次のように連鎖的に呼び出したい場合を考える。この f1 ~ f4 は Optional 型を返すものではなく、冒頭で述べたようなプロトタイプである場合を考える*1

f4( f4( f3( f2( f1(x), f1(y) ) ) ) )

まずこの時の x と y は Optional 値だとする。一般的な解は以下のようになるだろうか。

if(x and y){
    f4( f4( f3( f2( f1(x), f1(y) ) ) ) )
}

これは確かに単純で明快だが、この場合も、エラーチェックをラッピングしてやる事で if文を隠す事が可能だ。単純に、呼び出し時に引数が有効でなければ無効値を返し続けさせるラムダを構築してやれば良い。

template<class R,class... Param>
auto make_failable(R (*f)(Param...))
{
    return 
        [f](std::optional<Param>... xs) -> std::optional<R>
        {
            if((xs && ...)){
                return make_optional( f(*(xs)...) );
            }else{
                return std::nullopt;
            }
        };
}

これで、以下のように使う事ができる。

auto failable_f1 = make_failable(f1);
auto failable_f2 = make_failable(f2);
auto failable_f3 = make_failable(f3);
auto failable_f4 = make_failable(f4);

std::optional<int> result = failable_f4( failable_f3( failable_f2( failable_f1(x), failable_f1(y) ) ) );

x または y が無効値である場合、result には無効値がくる。これだけでも中々シンプルにまとまっているがさらに加えて、既存の関数 f1 ~ f4 が失敗する場合、つまり f1 ~ f4 が無効値を返してくる場合、結果も無効とするように make_failable を書き加える。

int f1(int a);
int f2(int b, int c);
std::optional<int> f3(int d); // f3 だけ Optional
int f4(int e);

これは単に先ほどの関数に加えて、以下のような関数をオーバーロードさせれば良い。

template<class R,class... Param>
auto make_failable(std::optional<R> (*f)(Param...))
{
    return 
        [f](std::optional<Param>... xs) -> std::optional<R>
        {
            if((xs && ...)){
                return f(*(xs)...);
            }else{
                return std::nullopt;
            }
        };
}

Reference:

*1:この動作は f1 の評価順序に依存しないものとする