Rokiのチラ裏

学生による学習のログ

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 の評価順序に依存しないものとする

C++20 進捗

先日、ISO C++委員会は、カナダのトロントで次の国際標準であるC++20の作業を開始し、技術仕様の開発が続かれたC++ 20ドラフトには、以下の機能が追加された。

  • Concepts
  • Explicit generic lambdas
  • _VA_OPT_
  • Default bit-field initializers
  • Fixed const-qualified pointers to members
  • Allow[=, this] as a lambda capture
  • Designated Initializers
  • More deduction guides for the standard library
  • Endian
  • Fixing string conversion fixes
  • Improve class template argument deduction in the stdlib
  • Arrays for make_shared
  • THREE Technical Specifications
    • Coroutines v1
    • Ranges v1
    • Networking v1


その他、reddit ページと内容が被るところがあるが、参考書執筆の際のリファレンスにするために、主に話題に上がっている提案書のリンクをリビジョン含めてまとめたので、もし提案の経緯など追いたければどうぞ。
github.com

最近 srook/cxx17/mpl/any_pack に加えた機能

テンプレートパラメータにautoを使える喜びを噛み締めて遊ぶ - Rokiのチラ裏 にて取り上げてから、いくつかの機能を追加したのでその紹介をしてみる。

cos table

[追記: any_pack によるコンパイル時コサインテーブルの構築。これは、srook/cxx17/mpl/any_pack/math/make_costable.hpp を使うと一発で生成できる。生成されたコサインテーブルは、今の所std::tupleで吐くようにしている。パラメータには x * y のように指定する。8 * 8 のコサインテーブルの生成は以下のようになる。

#include<srook/cxx17/mpl/any_pack/any_pack.hpp>
#include<srook/cxx17/mpl/any_pack/math/make_costable.hpp>
#include<srook/algorithm/for_each.hpp>
#include<iostream>

int main()
{
        constexpr auto t = srook::math::cos_table<8,8>; // コンパイル時にコサインテーブルを生成
        srook::for_each(t,[](const auto& x){std::cout << x << " ";});
        std::cout << std::endl;
}

この方法は非推奨とした。次の記述で同様のコンパイル時コサインテーブルが得られ、従来よりもオーバーフローなどの観点からみた時の安全性の向上が見込めるようになった。

constexpr std::array<const double, block * block> cos_table = srook::constant_sequence::math::unwrap_costable::array<srook::constant_sequence::math::make_costable_t<8,8>>::value;

– 追記終わり]
output:

1 1 1 1 1 1 1 1 0.980785 0.83147 0.55557 0.19509 -0.19509 -0.55557 -0.83147 -0.980785 0.92388 0.382683 -0.382683 -0.92388 -0.92388 -0.382683 0.382683 0.92388 0.83147 -0.19509 -0.980785 -0.55557 0.55557 0.980785 0.19509 -0.83147 0.707107 -0.707107 -0.707107 0.707107 0.707107 -0.707107 -0.707107 0.707107 0.55557 -0.980785 0.19509 0.83147 -0.83147 -0.19509 0.980785 -0.55557 0.382683 -0.92388 0.92388 -0.382683 -0.382683 0.92388 -0.92388 0.382683 0.19509 -0.55557 0.83147 -0.980785 0.980785 -0.83147 0.55557 -0.19509

costant_range

コンパイル時範囲変換。以前のエントリでは動的範囲への変換しか機能を提供していなかったので、コンパイル時に適用できる範囲(例えばタプルとか)への変換も用意した。

constexpr auto t = srook::any_pack<1,2,3>::constant_range<std::tuple>; // コンパイル時にタプルへ変換
static_assert( std::get<0>(t) == 1 and std::get<1>(t) == 2 and std::get<2>(t) == 3 );

また、単に範囲に変換するだけではなく、any_pack が保持している値に何らかの処理をしてから範囲へ変換するといったような操作もできる。無論、この時渡す関数オブジェクトは、constexprでなければならない。

struct Twice{
        explicit constexpr Twice()=default;
        template<class T>
        constexpr T operator()(T&& v)const
        {
                return v * 2;
        }
};

constexpr auto t = srook::any_pack<1,2,3>::constant_range<std::tuple,Twice>; // コンパイル時に any_pack が保持している各値を2倍してタプルに変換
static_assert( std::get<0>(t) == 2 and std::get<1>(t) == 4 and std::get<2>(t) == 6 );

make_any_pack

これは、指定サイズの any_pack の生成を行う。STL コンテナのコンストラクタのような感じでサイズと初期化値を指定する。パックに事前に値が含まれていた場合は、その後ろに値を付与する。

#include<srook/cxx17/mpl/any_pack/any_pack.hpp>
#include<boost/type_index.hpp>
#include<iostream>

int main()
{
        std::cout << boost::typeindex::type_id< srook::any_pack<>::make_any_pack<10,0> >().pretty_name() << std::endl; // 10 個の要素を 0 で初期化
        std::cout << boost::typeindex::type_id< srook::any_pack<1,2,3>::make_any_pack<10,0> >().pretty_name() << std::endl; // 1,2,3 の後ろに 10 個の 0 で初期化
}

output:

srook::mpl::v1::any_pack<0, 0, 0, 0, 0, 0, 0, 0, 0, 0>
srook::mpl::v1::any_pack<1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>

for_to, for_until, for_cut_to, for_cut_until

any_pack での for。Scala の for 文にインスパイアされて、to と until を採用している。これは機能が比較的多めなので指定できるパラメータ等を明記しておく。

srook::any_pack<>::for_(to | until | cut_to | cut_until)<begin, end, value を持つメタ関数, any_pack で包んだ任意の値, pack で包んだ任意の型, Invokable (constexpr)>

  • begin: ループで扱われる最初の値。Scala でのfor(i <- h until t) ...hに値する(begin は to であろうが until であろうが関係ない)。

  • end: ループで扱われる末尾、もしくは最後の値。for_toであれば Scala でのfor(i <- h to t)...tに値し、for_untilであれば Scala での for(i <- h until t)...tに値する。

  • value を持つメタ関数: valueを持つメタ関数を渡す。このvalueの値が any_pack に適用される。value を持つメタ関数は、以下のようなシグネチャでなければならない。

// 1st parameter std::size_t: begin ~ end 間の値
// 2nd parameter class: 呼び出し元の保持している any_pack
// 3rd parameter class: 呼び出し時に指定する任意の値の any_pack. 未指定の場合空の any_packが渡ってくる
// 4th parameter class: 呼び出し時に指定する any_pack で包んだ任意の型. 未指定の場合空の pack が渡ってくる
template<std::size_t,class,class,class>
struct Application;
  • any_pack で包んだ任意の値: any_pack で包んで任意の値を渡すことができる。渡すと、ループ中にその値を用いることができる。設定しなくても良い。
  • pack で包んだ任意の型: pack で包んで任意の型を渡す事ができる。渡すと、ループ中にその型を用いる事ができる。設定しなくても良い。
  • Invokable (constexpr): ループで扱われる値の増加、減少の幅をカスタマイズできる。constexprでの呼び出しが可能であり、かつconstexpr default constructibleが可能な関数オブジェクト型を指定する。設定しなくても良い。

例として以下に、この機能を用いたコンパイルfizzbuzz を示す。

#include<srook/cxx17/mpl/any_pack/any_pack.hpp>
#include<srook/algorithm/for_each.hpp>
#include<boost/type_index.hpp>
#include<iostream>
#include<tuple>

template<std::size_t,class,class,class> struct applyer;
template<std::size_t i,std::size_t... v>
struct applyer<i,srook::any_pack<v...>,srook::any_pack<>,srook::pack<>>
        : std::integral_constant<
                char,
                (srook::any_pack<v...>::template at<i>() % 15 == 0) ? 'F' :
                (srook::any_pack<v...>::template at<i>() % 5 == 0) ? 'b' :
                (srook::any_pack<v...>::template at<i>() % 3 == 0) ? 'f' : 'V'
        >{};

int main()
{
        using index = srook::any_pack<>::make_index_sequence<50>; // 0 ~ 49 の数列
        constexpr auto fizzbuzz = index::for_until<0,index::size(),applyer>::constant_range<std::tuple>; // fizzbuzz してタプルに変換
        srook::for_each(fizzbuzz,[](const auto& x){std::cout << x << " ";});
        std::cout << std::endl;
}

output:

F V V f V b f V V f b V f V V F V V f V b f V V f b V f V V F V V f V b f V V f b V f V V F V V f V

尚、指定された数値間での数値の増加/減少は、ユーザーが独自にカスタマイズできる。カスタマイズするには、constexpr で実行が可能な関数オブジェクトを渡す。

template<std::size_t i,class,class,class>
struct applyer : std::integral_constant<std::size_t,i>{}; // ループで増加する値をそのまま適用する

struct Increment{
        explicit constexpr Increment() = default;
        constexpr std::size_t operator()(std::size_t v)const noexcept{return v + 2;} // 2ずつ増加
};

using index = srook::any_pack<>::make_any_pack<4,0>;

constexpr auto t = index::for_until<0,4,applyer,srook::any_pack<>,Increment>::constant_range<std::tuple>;
static_assert( std::get<0>(t) == 0 and std::get<1>(t) == 2 and std::get<2>(t) == 0 and std::get<3>(t) == 0 and std::tuple_size<decltype(t)>::value == 4); // 2ずつ増加したため全ての範囲に行き渡らず途中で終わる。

この場合、2ずつ増加する。最後の数値を超えた場合、そこから先は処理されない。処理されなかった部分がいらなければ、for_cut_to、もしくはfor_cut_untilを使う。for_tofor_untilとの違いはここにある。

// applyer, Increment, index ....

 constexpr auto t = index::for_cut_until<0,4,applyer,srook::any_pack<>,srook::pack<>,Increment>::constant_range<std::tuple>;
 static_assert( std::get<0>(t) == 0 and std::get<1>(t) == 2 and std::tuple_size<decltype(t)>::value == 2); // 2ずつ増加したため全ての範囲に行き渡らず途中で終わり、行き渡らない範囲は切り捨てられる。

尚、この数値は増加だけでなく、減少も可能である。単純に、頭 > 最後の関係であれば、数値は減少していく。

constexpr auto t = index::for_until<4,0,applyer,srook::pack<>>::constant_range<std::tuple>; // 4 to 0
static_assert( std::get<0>(t) == 4 and std::get<1>(t) == 3 and std::get<2>(t) == 2 and std::get<3>(t) == 1 );

減少に対しても、増加と同じようにカスタマイズが可能である。

template<std::size_t i,class,class,class>
struct applyer : std::integral_constant<std::size_t,i>{}; // ループで減少する値をそのまま適用する

struct Decrement{
        explicit constexpr Decrement() = default;
        constexpr std::size_t operator()(std::size_t v)const noexcept{return v - 2;} // 2ずつ減少
};

using index = srook::any_pack<>::make_any_pack<4,0>;

constexpr auto t1 = index::for_until<4,0,applyer,srook::any_pack<>,srook::pack<>,Decrement>::constant_range<std::tuple>;
static_assert( std::get<0>(t1) == 4 and std::get<1>(t1) == 2 and std::get<2>(t1) == 0 and std::get<3>(t1) == 0 and std::tuple_size<decltype(t1)>::value == 4); // 2ずつ減少したため全ての範囲に行き渡らず途中で終わる。

constexpr auto t2 = index::for_cut_to<4,0,applyer,srook::any_pack<>,srook::pack<>,Decrement>::constant_range<std::tuple>;
static_assert( std::get<0>(t2) == 4 and std::get<1>(t2) == 2 and std::tuple_size<decltype(t2)>::value == 2); // 2ずつ減少したため全ての範囲に行き渡らず途中で終わり、行き渡らない範囲は切り捨てられる。

また、任意の値を渡すことができる。

template<std::size_t,class,class Value,class>
struct applyer : std::integral_constant<decltype(Value::first),Value::first>{}; // 全ての要素を渡ってきた任意の値にする

using index = srook::any_pack<>::make_any_pack<4,0>;

constexpr auto t = index::for_until<0,index::size(),applyer,srook::any_pack<42>,srook::pack<>>::constant_range<std::tuple>; // 任意の値 42 を渡す
static_assert( std::get<0>(t) == 42 and std::get<1>(t) == 42 and std::get<2>(t) == 42 and std::get<3>(t) == 42);

任意の型を渡す事もできる。

#include<iostream>
#include<srook/cxx17/mpl/any_pack/any_pack.hpp>
#include<srook/algorithm/for_each.hpp>

struct Twicer{
        explicit constexpr Twicer() = default;
        template<class T>
        constexpr T operator()(T v)
        {
                return v * 2;
        }
};

template<std::size_t,class,class,class> struct applyer;
template<std::size_t i,auto... v,class... Ts>
struct applyer<i,srook::any_pack<v...>,srook::any_pack<>,srook::pack<Ts...>>
        : std::integral_constant<decltype(i),srook::First_t<srook::pack<Ts...>>()(i)>{};

int main()
{
        constexpr auto t = srook::any_pack<>::for_until<0,4,applyer,srook::any_pack<>,srook::pack<Twicer>>::constant_range<std::tuple>;
        srook::for_each(t,[](const auto& x){std::cout << x << " ";});
        std::cout << std::endl;
}

output:

0 2 4 6

for_type_to, for_type_until, for_type_cut_to, for_type_cut_until

これは、上記の for と殆ど変わらないが、適用する値が value ではなく type 、つまり型をどんどん連結した any_pack を構築する for となっている。よって、適用するメタ関数には type という型が定義されていなければならない。

output:

f i z z b u z z 1 2 f i z z 4 b u z z f i z z 7 8 f i z z b u z z 11 f i z z 13 14 f i z z b u z z 16 17 f i z z 19 b u z z f i z z 22 23 f i z z b u z z 26 f i z z 28 29 f i z z b u z z 31 32 f i z z 34 b u z z f i z z 37 38 f i z z b u z z 41 f i z z 43 44 f i z z b u z z 46 47 f i z z 49

stack overflow からの小ネタメモ #2

前回

stackoverflow.com 質問者は、void_t detection idiom によるSFINAEを試行している。void_t には、確かにテンプレート型に依存するパラメータが渡されているが、C++14 N3936 [temp.alias] を参照すると、テンプレートパラメータのエイリアステンプレートに対して型を指定した時、それが未使用だった場合、テンプレート引数の置換が保証されるような文面がないのである。

template<class...> // 無名のテンプレートパラメータ
using void_t = void;

// value_type を持つ事を要求する
// C++14 では template-idが例え依存していたとしてもテンプレート引数の置換は義務ではない
template<class T,void_t<T::value_type>* = nullptr>
void f(T){}

解決法としては、使用すれば良いわけなので、以下のように make_voidなどを用意してやる。

template<typename...> struct make_void { typedef void type;};
template<typename... Ts> using void_t = typename make_void<Ts...>::type;

しかしこのような動作は、直感的ではないという事で、CWG Issue 1980として取り上げられている。これによれば、14.5.7 [temp.alias] に対して drafting されているとの事で、執筆時現在では、[temp.alias] にて確認できる。内容を引用。

However, if the template-id is dependent, subsequent template argument substitution still > applies to the template-id. [ Example: template<typename…> using void_t = void; template void_t f(); f(); // error, int does not have a nested type foo — end example ]

template-id が依存している場合は、それ以降のテンプレート引数の置換は引き続き template-id に適用される。つまり、これは先ほど述べた内容を保証するといった文面であるから、質問者のコードは、間違いなく C++1z 以降の処理系では正しく動作するべきである。そして、 C++14 では、void_t の内部実装を上記のような void_make などのようなやり方にして、C++1z と同じように動くべきである。

stackoverflow.com GCC と Clang で、それぞれstd::arrayのサイズを0として宣言する際に、default-constructible を要求するかが異なるといった質問だ。これは、[array.zero] にその動作が定義されている。ただ、その文面には、0サイズのstd::arrayをどのように実装すべきかについて執筆時現在言及されていない。つまり IDB なので、実装が default-constructible を要求したとしても、規格違反とはならないのである。しかし、これは規定すべきだという事で LWG issue 2157 として取り上げられている。LWG 2157 では、配列の不特定の内部構造は0サイズのstd::arrayの初期化を許可し、任意の型Tが default-constructible でない場合でも、有効でなければならないといった文面が提示されている。

stackoverflow.com 質問内容は、ローカルクラスがテンプレートメンバーを持つことができない明確な理由とは?といったもの。
回答内容から一部引用する。

template <typename T>
class X {
    template <typename>
    void foo() {
        T t;
    }
};

このクラス X に対して void を指定しても foo を呼び出さなければそれは ill-formed ではない。何故ならば、呼び出されなければ、あるいは明示的インスタンス化がされなければ*1インスタンス化されないからだ。続いて、さらに回答内容から一部引用。

template <typename T>
auto bar() {
    return [] (auto) {T t;};
};

このコードは ill-formed であるが、これを機能としてサポートする場合を考える。この場合、ローカルコンテキスト*2に依存しないように、ローカルのテンプレートを十分にインスタンス化する必要があるため、generic lambda の body 部分をテンプレート関数 bar のテンプレート引数 T を使って部分的にインスタンス化しなければならない。
テンプレート関数は先ほども述べた通りインスタンス化の遅延が施されるが、ジェネリックラムダも所謂テンプレート版関数オブジェクト、つまりローカルテンプレートクラスである。しかし、回答でも触れられている通り、特にテンプレートメンバ関数がテンプレート関数のローカルクラスの内部にある場合コンパイラ側の対応がとても困難である*3ため、このような部分的インスタンス化を行うような機能が導入されなかったのだとのこと。…しかし、ラムダの body 部分にテンプレートパラメータの型を書くという事は、割と自然にやってしまいそうなものだ。一応、CWG issue 728として取り上げられたようだが、extention となったようだ。CWG issue 728 で問いている内容に対する答えは、上記の通り処理系側の実装困難な問題であるというのが最も妥当だろう。実際、GCC はこのこの extention をサポートしている?ようだが当然 IDB なので、やはり推奨されるものでは決してないだろう。

stackoverflow.com C++にはデフォルト引数を再定義する機能がある([dcl.fct.default]/4)。この機能を有効的に使うシーンは何かあるだろうか?という内容。これに対して、既存のコードやライブラリを変更できず、実際に正しい引数を入力することができない場合、一部のスコープのデフォルト引数を変更することが、いくつかのレガシーツールで生成されたC++コードを扱うときに役立つハックのようなものとして利用できるとのアンサーが1つ付いている。例えば、生成されたコードがデフォルトの引数を使用して何百もの外部ライブラリに何百もの呼び出しを持っていたが、デフォルトの引数がもう正しくない場合など。確かに、そういう用途では検討しても良いのかもしれないが、混沌を更に深めるような気もしてくる。正直、デフォルト引数そのものがそこまで良いものとも思えないので、わざわざ積極的に利用するものでもないだろう。

*1:コンパイラオプション等による明示的インスタンス化を含む

*2:それを含む関数テンプレートのテンプレートパラメータを含む

*3:処理系の実装として、関数定義の処理中に使用される語彙スコープが基本的に一時的であることが知られているとのこと。

JPEG エンコーダ書いた

以前、JPEGに関する画像圧縮技術やデータ構造などついて学んだので、折角だし自分で作ってみようと思い、フルスクラッチで書いて見た。といっても、性能自体はそんなに良いものではないと思うし、簡単のために色々と制限を設けているので実用的ではないと思うが。言語はC++1z。Structured bindings とか Class templates deduction とか普通に使っているので、C++1z に対応しないコンパイラではコンパイルできない。

github.com

仕様と諸々の説明は以下の通り。

出力された lena 氏の画像*1。左はカラーモード、右はグレースケールモードで出力した。グレースケール画像は、簡単のために、Cb値とCr値を 0 としてそのまま持たせている。

f:id:Rok1:20170702130810j:plain:medium f:id:Rok1:20170702131116j:plain:medium

JPEG エンコーダをフルスクラッチで実装した事で、JPEG バイナリが読めるようになった。そしてデータ圧縮のロマンをより一層感じた。今後も時間があれば、機能拡張したりエフェクト付け加えたり、はたまたデコーダとかを書ければなと思う

※追記

その後、JPEG デコーダもフルスクラッチで実装した

*1:ブログに載せる上で画像の表示を小さくしている。