Quantcast
Channel: Flat Leon Works
Viewing all 120 articles
Browse latest View live

【Python】コマンドライン引数を解析する(argparseの利用)

$
0
0

Python2.7を対象とします。

コマンドライン引数の解析

コマンドライン引数の解析とは、Pythonスクリプト起動時に渡されたパラメータを解析することです。起動時に渡されたパラメータは'sys.argv'で取得できますが、パラメータの意味の解析やエラーチェックなどは自分で行う必要があります。そのへんを便利にしてくれるのがPythonの標準ライブラリであるargparseです。

argparseの便利なところ

  • 自動でhelpオプション(-h--help)を生成してくれる
  • 不正なパラメータを検知してエラーを出してくれる
  • 位置引数とオプション引数の両方が扱える
  • オプション引数に別名を設定できる( -f--fooなど )
  • その他いろいろ!

argparseを利用する

argparseの利用手順

  • 1.ArgumentParserオブジェクトを生成する
  • 2.ArgumentParserオブジェクトにパラメータ(引数)を追加していく
  • 3.ArgumentParserオブジェクトを使って起動パラメータを解析する
  • 4.解析結果を利用する
# 1.ArgumentParserオブジェクトを生成する
parser = argparse.ArgumentParser(description='MyScript')

# 2.ArgumentParserオブジェクトにパラメータ(引数)を追加していく 
parser.add_argument('inputFile', help='Input file path')
parser.add_argument('--outputFile', help='Output file path')

# 3.ArgumentParserオブジェクトを使って起動パラメータを解析する
args = parser.parse_args()

# 4.解析結果を利用する
fileData = open( args.inputFile, 'r' ).read()

メソッドなどの詳細は公式ドキュメントをご覧ください。

逆引きリファレンス

コマンドライン引数を指定して解析する

parse_argsメソッドにリストを渡すことで、コマンドライン引数を指定することができます。スクリプトの動作確認などに便利です。

args = parser.parse_args(['aaa.txt', '--outputFile', 'bbb.txt'])

ドキュメント : parse_args() メソッド

位置引数を追加する

parser.add_argument('inputFile', help='Input file path')

add_argumentメソッドに渡した名前の最初の文字が-でない場合は位置引数になります。位置引数はプログラムの必須のパラメータになるので、パラメータとして渡されなかった場合はエラーが発生します。(ただし、nargsで '?'か '*'を指定し、さらにdefault値を設定した場合は省略可能)

ドキュメント : add_argument() name-or-flags

オプション引数を追加する

parser.add_argument('--outputFile', help='Output file path')

add_argumentメソッドに渡した名前の最初の文字が-の場合はオプション引数になります。オプション引数は省略可能なパラメータです。(省略不可にすることも可能)

ドキュメント : add_argument() name-or-flags

オプション引数にデフォルト値を設定する

add_argumentメソッドのdefault引数を使うことでデフォルト値を設定できます。

parser.add_argument('--foo', default=42)

ドキュメント : add_argument() default

引数を文字列以外で受け取る(変換を行う)

add_argumentメソッドのtype引数を使うことで文字列以外の型として受け取ることができます。実際には文字列から別の型への変換が行われます。

parser.add_argument('foo', type=int) # 引数をintとして受け取る

typeにはPython組み込み型の他、関数を指定することもできます。

ドキュメント : add_argument() type

引数をユニコード文字列で受け取る(変換を行う)

defutf8_to_unicode( string ):
    returnunicode( string, 'utf_8' )
parser.add_argument('foo', type=utf8_to_unicode)

変換関数を定義してtype引数に渡しています。

ドキュメント : add_argument() type

オプション引数をフラグとして設定する

add_argumentメソッドのaction引数とconst引数を使うことでオプション引数をフラグとして設定することができます。

parser.add_argument('--debug', '-d', action='store_const', const=True, default=False )
args = parser.parse_args()

if args.debug:
  print'Debug mode'

引数に --debug or -dがあった場合、'args.debug'は True になります。

以下のような書き方もできるようです。

parser.add_argument('--debug', '-d', action='store_true' )

ドキュメント : add_argument() action

引数の個数を指定する

add_argumentメソッドのnargs引数を利用することで引数の個数(続く引数の消費数)を指定することができます。

N(整数)N個の引数を取りリストに格納される。Nが1でもリストに格納されることに注意。
?1個の引数を取る。引数が取れなかった場合はconst値になる。引数自体が存在しない場合はdefault値になる。
*複数の引数を取りリストに格納される。引数自体が存在しない場合はdefault値になる。
+複数の引数を取りリストに格納される。最低でも1つは必要。
argparse.REMAINDER残りの引数すべてを取りリストに格納される。
(nargs指定なし)action引数によって決まる。通常は1つのアイテムになる。

ドキュメント : add_argument() nargs

引数を複数回指定可能にする

add_argumentメソッドのaction引数に appendを指定することで引数を複数回指定可能になります。

parser.add_argument('--foo', action='append')
args = parser.parse_args('--foo 1 --foo 2'.split())
args.foo # ['1', '2']

複数指定された引数はリストに格納されます。

nargs='*'と併用した場合、リストのリストにはならず、単一のリストになるようです。

ドキュメント : add_argument() action

オプション引数を必須にする

add_argumentメソッドのrequired引数を利用することでオプション引数を必須にすることができます。

parser.add_argument('--foo', required=True)

ドキュメント : add_argument() required

引数の内容を限定する

add_argumentメソッドのchoices引数を利用することで引数の内容を限定することができます。指定以外のものが渡された場合はエラーになります。

parser.add_argument('move', choices=['rock', 'paper', 'scissors'])

ドキュメント : add_argument() choices


ゲームにおけるタッチスクリーンの問題点

$
0
0

タッチスクリーンの問題点はタイミングではない

モバイルのタッチスクリーンがゲームのコントロールとして最悪である理由を解明した研究論文、フィンランドの大学から | TechCrunch Japan

上記の記事ではタッチスクリーンの問題点としてタイミングが不定である点を挙げています。しかし個人的にはiPhoneでゲームをやっていてタイミングに問題を感じたことはありません*1。記事ではFlappy Birdを例にあげていますが、Flappy Birdでもタッチの反応が気になったことはありません。Flappy Birdは確かに難しいゲームですが、Flappy Birdの難しさは単純にタイミングのシビアさにあります。物理ボタンで遊んだとしても難易度はたいして変わらないでしょう。

しかし、タッチスクリーンには問題点がたくさんある

タッチスクリーンでゲームを遊ぶ際にタイミングによる問題はないと述べましたが、タッチスクリーンによる操作に問題がないとは言っていません。問題がないどころかおおありです。以下は個人的に感じたタッチスクリーンの問題点です。

  • 快感がない
  • 画面が指や手で隠れる
  • 遠いボタンのタッチに時間がかかる
  • ボタンの感触がないので空振りしやすい
  • 指でないと反応しない
  • 押したつもりがなくても反応してしまう

快感がない

タッチには物理ボタンを押したときのあの気持ちよさがありません。これはタッチスクリーンでのゲーム操作の一番の問題点かもしれません。

画面が指や手で隠れる

画面をタッチするので当然、画面が指や手で隠れてしまいます。画面が指で隠れてしまうと、現在の状況がわからなくなるという問題はもちろんありますが、対象が小さい場合にタッチするのが難しくなるという問題も発生します。タッチペンを使うという手段もありますが、タッチスクリーン対応のタッチペンは太いものが多く、反応も悪いような気がします。また、タッチペンだと2箇所同時にタッチできないので*2、2箇所以上同時にタッチするゲームでは使えません。

ちなみに携帯ゲーム機のPSVitaは背面タッチという画期的な機能を搭載していますが、あまり活用はされていないようです。そもそもPSVitaには物理ボタンがありますからね。ではiPhoneに背面タッチ機能がつけば、活用されるのかというと、されないような気がします。想像してみるとだいぶ使いづらいです。ただ、これは背面タッチで画面上のボタンをタッチしようとした場合です。照準をコントロールするというような場合には活躍するかもしれません。

遠いボタンのタッチに時間がかかる

タッチスクリーンのゲームでは画面上のボタンをタッチする必要がありますが、すぐに指の届く範囲にボタンがあるとは限りません。すぐには届かない場所にあるボタンをタッチするには、持ち方を変える必要があります。これは大幅な時間のロスになります。持ち方を変える必要がなくても指の移動に時間がかかります。また、遠くのボタンを押そうと指を伸ばせばその分だけ画面を隠すことになります。

ボタンの感触がないので空振りしやすい

ここでいう空振りとは目的のボタンでないところをタッチしてしまうことです。タッチスクリーン上のボタンは指の感触でボタンの位置を知ることができないので空振りしやすくなっています。

スクリーンに凹凸を与える技術(動画:変形して押せるボタンが現れるタッチスクリーン Tactus Tactile layer - Engadget Japanese)を使えばこの問題は解消できそうですが、まだ実現されていません。

指でないと反応しない

タッチスクリーンは基本的に指でないと反応しません。このことの何が問題かというと、画面上のボタンを爪でタッチしても反応しないということです。爪でのタッチは、そのつもりがなくてもボタンの位置によっては発生してしまいます。これも空振りになってしまいます。イライラです。

https://www.googledrive.com/host/0B9DcFxQIbTAFd3Z5Y0pfSXN5TG8/touch_screen_as_game_controller_01_e.png

押したつもりがなくても反応してしまう

タッチスクリーンは指というか肌でしか反応しませんが、逆にいうと指でなくても肌であれば反応してしまうともいえます。iPhoneを横持ちでゲームパッドのように持つと、親指の付け根で画面端をタッチしてしまうことがあります。これは特に親指で少し遠くのボタンを押そうとしたときになりやすいです。

https://www.googledrive.com/host/0B9DcFxQIbTAFd3Z5Y0pfSXN5TG8/touch_screen_as_game_controller_01_e.png

タッチスクリーンで激しいアクションゲームは可能か

これらの問題があるため、タッチスクリーンのデバイスはゲーム、特にアクションゲームには向いていないと思われるかもしれません。しかし、比較的高難易度なアクションゲームであるケロブラスターやDownwellは快適とまではいかないまでも、普通に遊ぶことができました(クリアもしました)*3

タッチスクリーンはアクションゲームには不向きですが、コンシューマにあるような普通のアクションゲームを普通に遊ぶことはできるのです。もちろん、ゲーム製作者側がスマホに考慮した操作方法にしておく必要があります*4。ケロブラスターやDownwellはしっかりと考慮されていると感じました。

まとめ

  • タッチスクリーンでゲームを遊ぶ際にタッチタイミングは大きな問題にならない
  • タッチスクリーンはアクションゲームには不向きとは言え、普通にアクションゲームを楽しむことはできるので、どんどんアクションゲームを出して欲しい
  • 手軽なスマホゲームパッドがもっと出て欲しい

*1:もしかしたら、iPhone以外のデバイスではタイミングの問題があるのかもしれません

*2:タッチペン2本持ちもできなくはないですが、そうすると本体は机に置くなどする必要があります

*3:もちろん遊ぶことが厳しい作品もあります。アーケードからの移植であるメタルスラッグは個人的には無理でした。

*4:よく使うボタンは左右2つずつの合計4つぐらいまでにしておくのが良さそうです。

Pillow(Python画像処理ライブラリ)でRGBAパレット形式のTGAファイルを保存する

$
0
0

PillowPythonの画像処理ライブラリです。PillowはTGAファイルに対応しているのですが、RGBAパレット形式のTGAファイルの保存ができなかったのでソースコードをいじって対応させてみました。

ちなみにRGB形式のパレットには最初から対応されていました。

注意事項

  • 確認したPillowのバージョンは'2.8.2'です。ただし、最新のリポジトリでもRGBAのパレットには対応してないようです。
  • 保存しか試していません。(読み込みは試していません)
  • ソースコードいじるのは自己責任でお願いします。
  • RGBAパレット形式のTGAファイルを正しく表示できるビューワーはあまりないようです。いくつかの画像ソフトで試してみましたが全部正しく表示されませんでした。TGAのフォーマット的にはRGBAのパレットは許可されていたと思うのですが…。

いじるファイルは以下のファイルだけです。ファイルの場所は、OSや環境によって変わると思います。私の環境(Mac)では以下の場所にありました。

/usr/local/lib/python2.7/site-packages/PIL/TgaImagePlugin.py

変更箇所

変更は_saveメソッド内の2箇所だけです。

160行目あたり :

        colormapfirst, colormaplength, colormapentry = 0, 256, 24

の次の行に以下のコードを追加

if im.im.getpalettemode() == 'RGBA':
            colormapentry = 32

186(188)行目あたり :

if colormaptype:
        fp.write(im.im.getpalette("RGB", "BGR"))

を以下のコードに差し替え

if colormaptype:
        if colormapentry == 32:
            fp.write(im.im.getpalette("RGBA", "BGRA"))
        else:
            fp.write(im.im.getpalette("RGB", "BGR"))

変更後の_saveメソッドです。これをコピーして差し替えるのが楽かもしれません。

おまけ : Pillowを使ったPNGからTGA(パレット形式)への変換

from PIL import Image
Image.open( 'hoge.png' ).convert( 'P' ).save( 'hoge.tga' )

Macでフォルダアイコンの色を変更する方法とGitの罠

$
0
0

Macで作業をしていて、ふとフォルダアイコンの色を変えたくなりました。調べてみたところ、Mac標準の機能だけでできることがわかったので紹介したいと思います。また、この方法はGitでちょっと問題がでるのでその対処法も紹介します。

フォルダアイコン色の変更手順

この動画を見たほうが早いかもしれません。紹介する方法もこの動画とほぼ同じものです。

How to Change Folder Colors in OS X - YouTube

  • 1 . Finder上で色を変えたいフォルダの「情報を見る」を選択 ->ファイル情報の小窓を表示されます
  • 2 . 小窓の左上のアイコンをクリックして選択し、コピー(Command+C)
  • 3 . Mac標準アプリの「プレビュー」を起動し、メニューの「ファイル」>「クリップボードから新規作成」を選択 ->フォルダアイコン画像がいくつか表示されます
  • 4 . 並んだフォルダアイコンの一番上が選択されていることを確認し、メニューの「ツール」>「カラーを調整」を選択 ->カラー調整のウインドウが表示されます
  • 5 . カラー調整ウインドウで適当に色を変えます。RGB値で変えられるわけではないのでちょっとコツがいります
  • 6 . カラー調整が終わったらカラー調整ウインドウを閉じて、コピー(Command+C)
  • 7 . Finderのファイル情報の小窓に戻り、小窓の左上のアイコンをクリックして選択肢、ペースト(Command+V) ->フォルダアイコンが変更されます

他のフォルダも同じアイコンにしたい場合は、ファイル情報小窓の左上のアイコンをコピー&ペーストすれば同じアイコンにすることができます。

フォルダアイコンを元に戻す方法

フォルダアイコンを変更すると、その変更情報が「Icon」という名前の隠しファイルとして保存されます。なのでこの「Icon」ファイルを削除すれば元に戻ります。隠しファイルをFinder上に表示させる方法はググッてください(笑)。個人的にはXtraFinderの拡張機能を使うのがおすすめです。

見た目を元に戻すだけなら、通常のフォルダのファイル情報小窓の左上のアイコンをコピー&ペーストすれば見た目は元に戻りますが、「Icon」ファイルは存在したままになります。「Icon」ファイルの存在が気にならなければこの方法でも良いでしょう。

Gitの罠

ところで、この「Icon」ファイル、Gitでうまく扱うことができません。Gitで扱おうとするとエラーが発生してしまいます。しょうがないので「Icon」ファイルをGitの無視ファイルリストに入れようとしても通常の方法ではうまくいきません。なぜならこの「Icon」ファイルの実際のファイル名は「Icon\r\r」だからです。

Icon」を無視リストへ追加する方法はいくつかあるようでしたが、私がやってうまくいったのはPythonで直接「Icon\r\r」を書き込む方法でした。

f = open('ファイルの場所/.gitignore_global','a+')
f.write( 'Icon\r\r' )
f.close()

他の方法はこの辺に。

osx - How to ignore Icon? in git - Stack Overflow

ちなみにVimを使って「Icon\r\r」を書き込む方法は私の環境ではうまくいきませんでした。

参考

【Lua組み込み】Luaスクリプトのファイル分割を独自のファイルロード処理で行う方法

$
0
0

require関数やdofile関数のファイルロード処理はカスタマイズできない

Luaスクリプトを書いていてファイル分割をしたくなった場合、Lua標準のrequire関数やdofile関数を使うと思います。

dofile( 'common.lua' )
dofile( 'util.lua' )

-- いろんな処理

問題なのはファイルロード処理を独自の方法で行いたい場合です。require関数やdofile関数はファイルロードにC標準関数のfopenを使うようになっており、ファイルロードをカスタマイズする仕組みも特に用意されていません。なので、ファイルのロード処理をfopenではなく別の方法で行わせたい場合はLuaソースコードを書き換えるしかありません。面倒くさいですね。

自分でファイルロード処理を行う関数を登録する

よく考えてみると、require関数やdofile関数にこだわる必要はありません。ファイルをロードしLuaスクリプトを実行するC関数を自分で作ってLuaに登録すればいいのです。

例えば、自作する関数の名前をincludeとした場合Luaスクリプト側はこうなります。

include( 'common.lua' )
include( 'util.lua' )

-- いろんな処理

このinclude関数の実装は以下のようになるでしょう。

// グルー関数の定義int lua_include_glue( lua_State* p_luaState )
{
    constchar* p_fileName = lua_tostring( p_luaState, -1 ); // include関数の引数を取得constchar* p_luaCode = MyFileLoad( p_fileName ); // 独自のファイルロード処理
    luaL_dostring( p_luaState, p_luaCode ); // 文字列をLuaスクリプトとして実行
    lua_settop( p_luaState, 0 ); // スタックをクリアreturn0; // Luaへの戻り値は無し
}
// グルー関数のLuaへの登録
lua_register( p_luaState, "include", lua_include_glue );

ポイントはluaL_dostringを使うことです。この関数を使うことでLuaにファイルロードさせるのではなく、自分でロードしたスクリプト文字列を実行させることができます。

まとめ

独自のファイルロード処理でLuaスクリプトをファイル分割したい場合は、自分で関数を作ってしまいましょう。

【メモ】気になるツールなど

$
0
0

そのうち試してみたいツールなどを忘れないようにメモ。

プログラム系

  • プログラミング言語
    • JavaScript : Webサイトだけでなくいろいろな場面で使われてるので覚えておいて損はなさそう。
    • Nim : インデント構文、静的型、強力なマクロ、そしてC言語コードを生成。まだ若い?
    • Haxe : モダンな文法。C言語コード生成可能。
    • Swift : すごく良さげ。C言語Python並にいろんなプラットフォームで動くようになるといいんだけど。
    • Scala : Swift並に良さげな言語。JavaVMから独立したら使いたい。
  • ライブラリ
  • 開発ツール
    • CMake : ビルドツール。よく使われている気がする。QtCreatorもCMakeをサポートし始めたらしい。
  • その他

グラフィック系

サウンド系

  • GarageBand : ピコピコサウンドも作れるらしい

ウェブ系

  • Greasemonkey : スクリプトによってウェブページの表示を変えることができるFirefoxアドオン。自分でスクリプトを書けるようになれば、効率的に情報収集ができるようになりそう。
  • Bootstrap : いい感じの見た目のWebサイトを手早く作るためのライブラリ
  • Sass : CSS拡張言語。CSS手書きはもういやだ。

その他

【C++ アイデア】関数内でメンバ変数を定義する

$
0
0

メンバ関数の中でメンバ変数の定義ができたら便利だろうなと思ったので、方法を模索してみました。半分ネタです。

やりたいのはこういう感じのことです。

class A
{
    int m_Value; // こっちは普通のメンバ変数public:
    void Func( void )
    {
        local int m_State = 0; // 関数内でメンバ変数を定義if ( m_State == 0 ){ m_State = 1; }
    }
};

マクロとstd::mapを使うことでそれっぽく実装できました。

#include <stdio.h>#include <map>#define LOCAL_VAR_DECL( type, var_name ) static std::map<void*, type> var_name#define LOCAL_VAR_INIT( var_name ) if ( var_name.find( this ) == var_name.end() ) var_name[this]#define LOCAL_VAR_DEF( type, var_name, init ) LOCAL_VAR_DECL( type, var_name ); LOCAL_VAR_INIT( var_name ) = init#define LOCAL_VAR( var_name ) var_name[this]class A
{
public:
    void Func( void )
    {
        LOCAL_VAR_DEF( int, m_State, 0 ); // 関数内でメンバ変数を定義       #define m_State LOCAL_VAR( m_State ) // m_State でアクセスできるようdefineしておくswitch( m_State )
        {
            case0:
            {
                printf( "m_State = 0\n" );
                m_State = 1;
                break;
            }
            case1:
            {
                printf( "m_State = 1\n" );
                m_State = 0;
                break;
            }
        }
    }
};

https://ideone.com/C1rUV5

std::map<void*,型>でthisポインタをキーにしてアクセスすることで、メンバ変数のようなものを実現しています。std::mapの変数を関数内のstatic変数として定義しているので他の関数からは参照できませんが、関数外でLOCAL_VAR_DEFを使えば他の関数からも利用できると思います。

ところで、この実装は問題点がたくさんあります。

  • インスタンスが破棄されたあとも、動的メンバ変数データが残り続けてしまう。
  • インスタンスが破棄されたあとに、同じアドレスに同じクラスのインスタンスが生成された場合に、前のインスタンスのデータを参照してしまう。
  • アクセスするたびに、mapを検索するので効率が悪い。

このように致命的な問題があるので、実際に動くコードでは使ってはいけません。

ではどこで使うのかというと、以下の様な場面では役に立つかもしれません。

  • デバッグなどで一時的にメンバ変数を追加したい場合
  • メンバ関数の実装中に新しいメンバ変数が欲しくなったときに、とりあえずその場でメンバ変数を定義したい場合(あとで正式にメンバ変数を定義する)

どちらにしてもリリース版には残さないようにしましょう。

【ゲームプログラミング】ビット演算による点滅処理

$
0
0

ゲームプログラミングキャラクターを点滅させたいとき、以下のような実装にするかもしれません。

// C++class Player
{
    int m_DrawCount;
public:
    Player() : m_DrawCount( 0 ){}
    void Draw( void )
    {
        // 2フレームに1回描画if ( m_DrawCount % 2 == 0 )
        {
            // 実際の描画処理
        }
        ++m_DrawCount;
    }
};

2で割ったあまりが0の場合のみ描画してます。2で割ったあまりは0か1になるので、2フレームに1回描画することになります。 この実装でも特に問題はないのですが、ビット演算(&)を使うと効率的かつバラエティに富んだ点滅をさせることができます。

// C++class Player
{
    int m_DrawCount;
    uint8_t m_BlinkMask;
public:
    Player() : m_DrawCount( 0 ), m_BlinkMask( 1 ){}
    void Draw( void )
    {
        // 一致するビットが1つでもある場合描画if ( m_DrawCount & m_BlinkMask )
        {
            // 実際の描画処理
        }
        ++m_DrawCount;
    }
};

m_BlinkMaskを1にすると2フレームに1回の描画になります。しかも割り算が発生しない分高速です。

  • 0フレーム目 : 0 & 1 -> 0b0000 & 0b0001 -> 0b0000 ->偽 ->描画なし
  • 1フレーム目 : 1 & 1 -> 0b0001 & 0b0001 -> 0b0001 ->真 ->描画あり
  • 2フレーム目 : 2 & 1 -> 0b0010 & 0b0001 -> 0b0000 ->偽 ->描画なし
  • 3フレーム目 : 3 & 1 -> 0b0011 & 0b0001 -> 0b0001 ->真 ->描画あり
  • 4フレーム目 : 4 & 1 -> 0b0100 & 0b0001 -> 0b0000 ->偽 ->描画なし
  • 5フレーム目 : 5 & 1 -> 0b0101 & 0b0001 -> 0b0001 ->真 ->描画あり

m_BlinkMaskを変えるとさまざまなパターンの点滅になっていきます。

例えば、m_BlinkMaskを2にすると以下のように2フレームごとに描画のon/offが切り替わります。

  • 0フレーム目 : 0 & 2 -> 0b0000 & 0b0010 -> 0b0000 ->偽 ->描画なし
  • 1フレーム目 : 1 & 2 -> 0b0001 & 0b0010 -> 0b0000 ->偽 ->描画なし
  • 2フレーム目 : 2 & 2 -> 0b0010 & 0b0010 -> 0b0010 ->真 ->描画あり
  • 3フレーム目 : 3 & 2 -> 0b0011 & 0b0010 -> 0b0010 ->真 ->描画あり
  • 4フレーム目 : 4 & 2 -> 0b0100 & 0b0010 -> 0b0000 ->偽 ->描画なし
  • 5フレーム目 : 5 & 2 -> 0b0101 & 0b0010 -> 0b0000 ->偽 ->描画なし
  • 6フレーム目 : 6 & 2 -> 0b0110 & 0b0010 -> 0b0010 ->真 ->描画あり
  • 7フレーム目 : 7 & 2 -> 0b0111 & 0b0010 -> 0b0010 ->真 ->描画あり

ちなみに、このように同じ割合でon/offを繰り返すのはm_BlinkMaskの値を2のn乗にしたときのみで、他の場合はonの割合が多くなります。

m_BlinkMaskの値を63まで試したGif動画を用意したので、参考にどうぞ。

on/offを逆にしたバージョン


Macで60fpsの画面キャプチャGIF動画を作成する方法

$
0
0

普段、MacでのGif動画用の画面キャプチャはLICEcapというソフトを使っていますが、このソフトで画面キャプチャを行うとフレームレートが30fpsくらいになってしまいます。たいていは30fpsもあれば十分なのですが、ゲームの動画など60fpsにしたい場合もあります。

最近、60fpsの画面キャプチャGIF動画を作る必要があったのですが、Macでやる方法がなかなか見つからなかったのでまとめてみます。

手順

  • QuickTimePlayerで画面キャプチャを行う -> movファイル
  • InstagifferでmovファイルをGifファイルへ変換する

QuickTimePlayerの画面収録機能を使うと、60fpsで画面キャプチャできます。ファイルはmov(H.264)形式になります。そしてこの動画をInstagifferを使ってGif動画へと変換します。

Instagifferで60fpsGIF動画へ変換する

InstagifferはYoutubeからダウンロードしてGifへ変換する機能がメインのようですが、手持ちのファイルを変換することもできます。

Instagifferは動画の尺を変更したりキャプションを入れたりエフェクトを掛けたりできるようですが、単純に動画を変換したいだけの場合、逆にそれらを使ってしまわないように注意が必要です。

Instagifferを使って動画をそのまま変換するための手順です。

  • 動画ファイルパスを入力する
  • Lengthを0にする(0にすると尺変更なし)
  • fpsを60にする
  • Qualityを100にする
  • エフェクトウインドウを表示してEnhanceを無効にする
  • 「Create GIF!」ボタンを押してGifを生成する

不要なフレーム(コマ)を削除したい場合は、右側のプレビュー画面でフレームを選択しダブルクリックで削除することができます。

おまけ

LICEcapで作成した画面キャプチャGIF(30fps)

QuickTimePlayer->Instagifferで作成した画面キャプチャGIF(60fps)

おまけ2

他に試したツール。どちらもキャプチャではなく変換ソフト。

  • PicGIF Lite : 60fpsで生成されませんでした。
  • Gifrocket : 60fpsで生成されたけど、画質が劣化。と思いきや、デフォルト設定でリサイズされていただけだった。リサイズしないように設定したら綺麗にでた。悪くないかも。

【開発中】2Dアクション「センメツコースター(仮)」

$
0
0

2016年4月から「センメツコースター(仮)」というiOS用のゲームを作っています。

2016:08:18 20-37 from shinya on Vimeo.

ジェットコースター+アクションゲームな感じです。

最初は1ヶ月で完成させることを目標にしていましたが、4ヶ月も経ってしまいました。ゲーム開発って怖い…。 まだ完成の目処は立っていませんが、2016年内リリースを目標にしています。

どうか完成しますように🙏

Xcode7.3へのアップデート後、C++プロジェクトのビルド時間が異様に長くなる問題への対処

$
0
0

Xcode7.3へアップデート後、QtCreatorのC++プロジェクトのビルド時のリンク時間がやたら遅いなと思っていましたが解決法が書かれたページを見つけ、試してみたところ治ったので共有したいと思います。

Xcode自体でのビルドではなく、Xcode付属のClangを使ったときに発生した問題です。私の環境ではXcode自体でのビルドでは問題ありませんでした。

参考(感謝!): Extremely slow linking with clang, ld from XCod... | Apple Developer Forums

問題

QtCreatorのC++プロジェクトのビルド時のリンク時間がXcode7.3へのアップデート前は1〜2秒だったのが、アップデート後25〜30秒になった。これはデバッグビルド時のみ発生し、リリースビルド時はアップデート前と同じように1〜2秒でリンクが完了する。

解決法

デバッグビルド時のコンパイラオプションに-O0 -fvisibility-inlines-hiddenを与える。

ノート

リンク時間が長くなったのをXcode7.3へのアップデート後と書きましたが、その前にMacOSを10.10から10.11へアップデートしたり、QtCreatorをバージョンアップしたりしていたので、もしかしたらそちらに原因があった可能性もなきにしもあらず…。いや、たぶんXcode7.3へのアップデートが原因だと思いますが。

Gitリポジトリの統計情報を見るためにGitStatsを使ってみる(Macで)

$
0
0

プログラミングをしていてソースコードの行数が一体どれくらいになっているのか気になることがよくあるのですが、Gitリポジトリを解析して統計情報を生成してくれるGitStatsというツールがあることを知りました。ソースコード行数はもちろん、ファイル数、コミット回数などの推移も見ることができます。

公式サイト GitStats - git history statistics generator

どんな感じで表示されるのかはこちらの記事で詳しく紹介されています。

Gitリポジトリに蓄積された歴史を可視化、グラフ化する·GitStats MOONGIFT

今回はメモもかねて私がMacでGitStatsを利用するためにやったことを記しておきたいと思います。

手順

試した環境はMacOSX 10.11です。

大まかな手順は以下の通り

gnuplotをインストール

GitStatsはgnuplotを使うようなのでgnuplotをインストールします。もちろんすでにインストールされている場合は不要です。

gnuplot公式サイトcontributed executables for OSXというページからMac用実行ファイル(dmg)が入手できるのでそれを利用します。今回はgnuplot-5.0.5-quartz.dmgというファイルをダウンロードしました。とりあえず最新バージョンを選んでおけばいいと思います。

ダウンロードしたら他の一般的なMacアプリケーションと同じようにdmgファイルをダブルクリックで展開してgnuplotアプリケーションをアプリケーションフォルダなどへコピーします。

パスを通す

このままだとGitStatsはgnuplotの場所がわからないのでパスを通します。具体的にはgnuplotの実行ファイルのシンボリックリンク/usr/local/bin/に貼ります。

ターミナルを起動して以下のコマンドを実行します。これはgnuplotアプリケーションをアプリケーションフォルダに配置した場合です。

ln -s /Applications/Gnuplot.app/Contents/Resources/bin/gnuplot /usr/local/bin/

/Applications/Gnuplot.appからではなく、さらにその中にあるContents/Resources/bin/gnuplotからシンボリックリンクを貼ることに注意。.appはバンドルと呼ばれるただのフォルダで実行ファイルはその中にあります。

シンボリックリンクを貼ったら、ターミナルでgnuplotと入力してgnuplotが起動するか確認してみます。

GitStatsをGitHubから入手

ターミナルを起動して、適当な場所に移動してから以下のコマンドを実行します。

git clone git://github.com/hoxu/gitstats.git

GitStatsをインストール

git cloneで入手したgitstatsディレクトリに移動して以下のコマンドを実行します。

make install

これでGitStatsのインストールは完了です。

GitStatsを使ってみる

ターミナルを起動して以下のコマンドを実行します。

gitstats 【リポジトリ(.git)の場所】 【生成場所】

コマンドを実行すると指定した生成場所に40個ぐらいのファイルが生成されます。その中にあるindex.htmlをブラウザで開けばリポジトリの統計情報を見ることができます。

ところで、GitStatsが生成した統計情報の行数(Lines)項目はバイナリファイルでもカウントされているようです。FilesタブのExtensions項目で拡張子ごとの行数が見れるので、ソースコードの行数を知りたい場合はそこを見たほうがいいかもしれません。

【C++ アイデア】複数の名前空間に同じ別名を与える

$
0
0

C++では名前空間に別名を与えることができます。

namespace HogeCore {}
namespace Hoge = HogeCore;

しかし、複数の名前空間に同じ別名を与えることはできません。

namespace HogeCore {}
namespace HogeGraphics {}
namespace HogeAudio {}

namespace Hoge = HogeCore;
namespace Hoge = HogeGraphics; // コンパイルエラーnamespace Hoge = HogeAudio; // コンパイルエラー

ちなみにサンプルコードの意図としては、Hogeというライブラリの内部に複数の名前空間が存在し、ライブラリのユーザーにはすべてHogeという名前(名前空間)でアクセスできるようにさせたい、というものです。

C++では複数の名前空間に同じ別名を与えることはできませんが、別の方法で実質的に同じようなことを行うことができます。その別の方法とは、別名用の名前空間を作り、そのなかでusing namespaceを使うことです。

namespace HogeCore { class A{}; }
namespace HogeGraphics { class B{}; }
namespace HogeAudio { class C{}; }
 
// 別名用の名前空間を作り…namespace Hoge {
    // 使用可能にしたい名前空間をusing namespaceするusingnamespace HogeCore;
    usingnamespace HogeGraphics;
    usingnamespace HogeAudio;
}
 
int main()
{
    Hoge::A a;
    Hoge::B b;
    Hoge::C c;
    return0;
}

注意点としては、名前空間を1つにまとめることで名前の衝突が起こる可能性があります。乱用はしないようにしましょう。

参考: c++ - Multiple aliases for a namespace? - Stack Overflow

【センメツコースター】ライセンス表記

$
0
0
-------------------------------
Lua
-------------------------------

/******************************************************************************
* Copyright (C) 1994-2012 Lua.org, PUC-Rio.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************/

-------------------------------
picojson
-------------------------------

Copyright 2009-2010 Cybozu Labs, Inc.
Copyright 2011-2014 Kazuho Oku
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

-------------------------------
alure
-------------------------------

Copyright (c) 2009-2010 Chris Robinson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

-------------------------------
libogg
-------------------------------

Copyright (c) 2002, Xiph.org Foundation

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

- Neither the name of the Xiph.org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-------------------------------
libvorbis
-------------------------------

Copyright (c) 2002-2008 Xiph.org Foundation

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

- Neither the name of the Xiph.org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

iOS用2Dアクションゲーム「センメツコースター」の紹介

$
0
0

長い間作っていたゲームがようやく完成しました。

センメツコースターはiOS用2Dアクションゲームです。広告無しの有料アプリ(240円)です。 レールの上をコースター(乗り物)で移動して敵を倒す(センメツ)するのが目的です。

こんな人におすすめです。

  • iPhoneで仮想パッドを使ったアクションゲームをやりたい人
  • ちまちま動くのを見るのが好きな人
  • ロッコが好きな人

5つのワールドにそれぞれ5ステージで合計25ステージあります。クリアするだけなら1〜2時間くらいのボリュームだと思いますが、各ステージでスコアレートで星3つを目指したり、チャレンジモードで遊んだりすればもうちょっと長く遊べます。

正直地味なゲームですが、丁寧に作ったつもりなので楽しんでいただけたらうれしいです。

※ゲーム内の音楽はPANICPUMPKINさんのフリー素材を使わせていただきました。

スクリーンショット

AppStoreリンク


ゲームエンジンを使わずにC++とOpenGLでゲームを作った話

$
0
0

先日、iOS用ゲームアプリ「センメツコースター」をリリースしました。このゲームの開発にはゲームエンジンは使っておらず、C++OpenGLOpenALなどで実装されています。最近はゲームエンジンを使うことが当たり前になっており、ゲームエンジンを使わないゲームの作り方があまり知られていない気がしたので「センメツコースター」を例にして、どうやってC++OpenGLでゲームを作るのかを(大雑把に)紹介したいと思います。記事中で取り上げたツールやライブラリへのリンクは最後にまとめて掲載してます。

作業環境

作業はすべてMac上で行いました*1が、基本的にクロスプラットフォームなツールやライブラリしか使っていないのでWindowsでも同じように開発できると思います。また、「センメツコースター」はiOS用アプリですが開発自体はMac上で行っていたので、PCゲーム開発の話として読んで下さい。iOS対応の話はまた別の機会に…。

最低限必要なもの

まず、そもそもゲームを作るとはどういうことなのか。ゲームは何で出来ているのか。大きく分けると以下の要素になります。

  • ソフトウェアの生成
  • 絵の表示
  • 音の再生
  • 入力の受付
  • ファイルの読み書き
  • メインループ

ソフトウェアの生成とファイル読み書き以外、すべて素のC++では実現できません。外部のライブラリに頼る必要があります。逆にいうと、これらさえ用意できればあとはC++でコードを書くだけでゲームを作ることができます(理論的には)。

ソフトウェアの生成 : C++コンパイラ

ゲームとは何なのか。実態はソフトウェアです。アプリケーション、実行ファイルとも言います。ソフトウェアを生成する手段はいろいろありますが、「センメツコースター」ではC++コンパイラを使っています。つまりC++コードをコンパイルしてソフトウェアを生成しているわけです。

C++コンパイラはいろいろな種類がありますが、今回はClangを使いました。WindowsだとVC++が一番手軽かもしれません。コンパイラは直接使うことも出来ますが、一般的には統合開発環境(IDE)経由で使います。C++統合開発環境もいろいろありますが、私はQtCreatorを好んで使っています。QtCreatorはクロスプラットフォームなのでWindowsでも使うことができます。他の統合開発環境MacだとXcodeWindowsだとVisualStudioが有名です。

絵の表示 : OpenGL + GLFW

次に必要なのは絵の表示です。素のC++では絵の表示を行うことができないので外部ライブラリに頼ることになります。C++を使ったゲーム開発で一般的なのはOpenGLDirectXです。記事のタイトルにもあるように今回はOpenGLを選びました。OpenGLの魅力は何と言っても動作環境の多さです。MacWindowsはもちろんiOSAndroidでも動作します。ただしOpenGLと言っても1枚岩ではなく、複数のバージョンとOpenGLESというコンパクト版があります。OpenGLESは組み込み用のOpenGLなので、Mac,WindowsOpenGLのみに対応、iOS,AndroidはOpenGLESのみに対応しています。ですが、OpenGLESはOpenGLのコンパクト版なので、OpenGLESを使っておけばそのままOpenGL環境でも動作します(バージョンは合わせる必要があります)。「センメツコースター」ではOpenGLES2.0を選びました。

ところで絵の表示には、絵を表示する場所も用意する必要があります。絵を表示する場所とは、PCゲームの場合はウインドウのことです。また、ウインドウを用意した上でOpenGLを利用できるように関連付けを行う必要もあります。これらをまとめてやってくれるのがGLFWというライブラリです。GLFWを使うことで、簡単にOpenGLの描画先となるウインドウを用意することができます。さらにGLFWは入力の受付やメインループを作成するための機能も持っています。OpenGLでゲームを作るためにうってつけのライブラリです。

音の再生 : OpenAL + ALURE

効果音やBGMの再生もゲームには必須でしょう。音の再生も素のC++では行うことができないので外部のライブラリが必要になります。「センメツコースター」ではOpenALを選択しました。OpenALは名前からわかるようにOpenGLの音声版のような存在です。OpenALOpenGLと同じように多くの環境で動作します。OpenGLとは違いコンパクト版(組み込み版)や大きなバージョン違いはありません。

OpenALは音の再生を行うことができるのですが、wavやoggのような音声ファイルを読み込むことはできません。そこで利用するのがALUREというライブラリです。ALUREはOpenALのユーティリティライブラリです。音声ファイルを読み込んでOpenALへ渡してくれます。

入力の受付(マウス、キーボード、ゲームパッドなど) : GLFW

ゲームにはユーザーからの入力を受け付ける処理も必要です。これも素のC++ではできません*2。これには「絵の表示」のところでも出たGLFWが利用できます。

ファイルの読み書き : C言語標準関数

ゲームにセーブ機能をつける場合、ファイルの読み書き機能が必要になってきます。セーブ機能をつけなくても、テクスチャファイルなどの読み込みで必要です*3。これはC言語の標準関数、fopenとfwriteで対応可能です。fopenとfwriteはC言語の標準関数なので基本的にはどの環境でも動きます*4

メインループ : GLFW

最後に必要なのは定期的にゲームの更新処理と描画処理を実行することです。これをメインループと呼びます。メインループは正しく一定の周期で回す必要があるのですが、それを行うにはモニタの垂直同期待ちを行うか高精度タイマー(時間計測)が必要です。どちらも素のC++では実現できないことですが、GLFWにはどちらも用意されています。

現実的に必要になってくるもの

ここまででゲームを作るために必要最低限のものが揃いました。しかし、このままゲームを作り始めようとしてもいろいろ足りないことに気がつくでしょう。そんな足りないものの中でも特に必要になってくるであろうものを紹介します。これらは自分で実装してもいいし、他の人が作ったライブラリを使ってもいいでしょう。

OpenGLOpenALラッパー

描画を行うために必要なOpenGLですが、OpenGLAPIは使いづらい上に低レベルなので直接使うのではなく扱いやすくしたラッパークラス、ラッパー関数経由で使うことになると思います。

OpenALも同様です。

数学系クラス、関数

  • ベクトル
  • 行列
  • 矩形
  • 当たり判定その他の処理

ベクトルは座標データとして使いますし、当たり判定などでも使います。行列はOpenGLで頂点データを移動回転拡縮をさせるのに必要です。当たり判定はアクションゲームでは特に必要になってくる処理です。

これら数学系クラス、関数は自分で実装するよりすでに存在するライブラリを利用した方がいいかもしれません。なぜならその方が正確かつ高速な可能性が高いからです。「センメツコースター」では全部自分で実装しましたが…。

画像ファイルローダー

画像ファイルローダーとは画像ファイルを読み込んで解析しOpenGL用テクスチャデータとして変換する機能のことです。「センメツコースター」ではTGAファイルのローダーのみ作りました。TGAは簡単なフォーマットなので自作しましたが、pngなどの複雑なフォーマットの場合はライブラリを利用したほうがいいでしょう。

フォント描画システム

フォント描画システムは必須ではありませんが用意しておかないと、テキストを描画する箇所でいちいち画像データを用意する必要が出てきます。フォント描画システムの実装は、まともにやろうとするとかなり大変です。一番楽に必要最低限の実装をするとしたら、文字を0-9A-Zの範囲のみで固定文字幅のフォントテクスチャをペイントソフトなどで用意することでしょうか。固定文字幅でないテキスト描画や、日本語にも対応しようとするとかなり実装難易度が上がります。もしかしたらゲームエンジンを使わなかったことを一番後悔する場面がフォント描画システムを作るときかもしれません。テキストを描画したいだけなのになんでこんなに苦労しているんだろうと。

「センメツコースター」では、フォントテクスチャ生成ツールを作成し、そのツールから生成されたテクスチャデータと文字情報テーブル(jsonファイル)からテキストを描画するシステムを実装しました。

タスクシステム的なもの

ゲームは画面内にいろいろな物が表示されます。そしてよく動きます。これを実現するための古典的な方法がタスクシステムです。別にタスクシステムである必要はないのですが、個々のオブジェクトが毎フレーム更新処理を呼び出される仕組みはゲームと相性がいいのでまず必要になってくると思います。

参考: 【C++ ゲームプログラミング】STLで実装する最小のタスクシステム - Flat Leon Works

さらに実装、導入したもの

「最低限必要なもの」と「現実的に必要になってくるもの」を紹介してきました。ここでは「センメツコースター」でさらに実装、導入したもの紹介します。

UTF8対応文字列クラス

C++には文字列クラスとして標準でstd::stringがありますがUTF8に対応しておらず*5、日本語を含む文字列の場合1文字1文字を正確に扱うことができません。これが問題になるのは、例えばフォント描画システムで日本語を描画する場合などです。またstd::stringは機能も少ないといった不満もあったので、独自に文字列クラスを作成しました。

GUIシステム

GUIシステムとは、ボタンやウインドウなどのGUIを実装するための仕組みです。具体的には以下のような機能群です。

  • Widget(Form)のような共通の基底クラス
  • Widgetの親子関係(位置や表示状態の連動、マウスイベントなどの伝搬制御)
  • マウスやタッチイベント発生時のコールバック
  • ボタンなどの汎用的機能の提供

このようなGUIシステムを用意することでゲーム中のUIの実装が楽になります。またデバッグ機能やツールを作る場合にも利用できます。

Jsonの導入

Jsonとは汎用データフォーマットです。Jsonを使うことで、構造化されたデータをファイルに書き出し/ファイルから読み出しすることができるようになります。ゲームでは、設定ファイルやセーブデータのフォーマットとして利用できます。Json以外の選択肢としてはxml、ini、バイナリなどがありますが、Jsonが一番扱いやすいと思います。Jsonをバイナリ化し高速化したMessagePackというものもあります。

Jsonを利用するためにはJsonフォーマットの読み取りと書き出し処理が必要ですが、「センメツコースター」ではPicoJsonというライブラリを利用しました。

Luaの導入

Luaは軽量スクリプト言語です。単体で使うのではなくプログラムに組み込んで使うことに特化しています。Luaは柔軟なのでC++では大量にコーディングする必要があることを数行で書くことができるようになります。「センメツコースター」ではLuaをゲーム全体の制御と敵生成処理ルーチン、各種イベント、チュートリアルの実装に使っています。特にゲーム全体の制御にLuaを使うことの効果は抜群で、今はLuaを使わずにゲームを作ることは考えられないくらいです(個人の感想です)。

各種データ作成ツールのファイルローダー

データ作成ツールというのは、例えばドット絵エディタやマップエディタのことです。「センメツコースター」ではドット絵エディタとしてAseprite、ドット絵のアニメーションデータエディタとしてDarkFunctionEditor、マップエディタとしてTiledMapEditorを利用し、それぞれのファイルローダーを実装しました。*6

まとめ

以上が「センメツコースター」を作るためにやったことです。もちろん実際にはもっとたくさんのことをやったのですが、C++OpenGLでゲームを作るという意味ではだいたいカバーできていると思います。ただ、さらっと書いてますがいろいろな場面で苦労は多いです。例えばOpenGLで板ポリを出すだけでも相当つまづくポイントが多いです。ですが、この記事で紹介しているようなことを実装できた時点でもう自分だけのゲームエンジンが出来ているようなものです。あとは自分の好きなようにゲームエンジンを強化してくだけです。楽しい!でも、ゲームエンジンばかり作り込んでしまいゲームが完成しないということには注意しましょう。おすすめなのは実際にゲームを作りながら必要になった機能だけゲームエンジンに追加していくことです。

リンク集

*1:ねねっち誰かさんみたいにMacWindowsを積んだりもしていません

*2:標準入力という仕組みでキーボードからの文字入力を受け付けることはできますがゲームとしては使い物になりません

*3:テクスチャデータをC++の配列などで作ることもできますが、現実的ではないでしょう

*4:環境ごとに用意されたAPIを使わないといけない場合や使った方が効率が良い場合もあります

*5:対応はしていませんがUTF8の性質上、文字単体を扱うことを考慮しなければ普通に使えます

*6:この記事の趣旨から離れるのでこれらのツールの説明は省きます

2Dアクションゲーム「センメツコースター」の開発で使ったもの

$
0
0

先日「センメツコースター」の開発が完了しました。そこで開発に使ったものを紹介したいと思います。

制作ツール

IDE : QtCreator

QtCreator : Mac, Windows

C++IDEです。個人的に好きなIDEです。

テキストエディタ : Visual Studio Code

Visual Studio Code : Mac, Windows

C++のコードはIDEでコーディングしていましたが、Luaやその他のテキストはVisual Studio Codeを使っていました。最初はSublime Text2を使っていたのですが、次にAtomを使い始め、最終的にVisual Studio Codeにたどり着きました。

ドット絵作成 : Aseprite

Aseprite(有料) : Mac, Windows

ドット絵作成にはAseprite(v1.1.3)を使いました。Asepriteは有料ですが、使い勝手がよくアニメーションの作成も可能です。Asepriteの良いところはゲーム開発での利用に特化した機能が備わっていることです。具体的には以下のような機能です。

  • スプライト情報のJsonフォーマットなどでの出力機能
  • TGA書き出し
  • パック化(複数のスプライトを1枚のテクスチャにまとめる機能)
  • これらをコマンドラインから利用することが可能

パック化は例えばこんな感じでびっちりと敷き詰めてくれます。

スプライトアニメーション作成 : DarkFunctionEditor

DarkFunctionEditor(Java製) : Mac, Windows

DarkFunctionEditorは、スプライトのアニメーションデータを作成するためのツールです。複数のスプライトを組み合わせてセルを作り、それを並べてアニメーションを作ります。Asepriteにもアニメーション機能はありますが、このツールのようにスプライトを組み合わせてアニメーションを作るということができません。DarkFunctionEditorはタイムラインや各種補間と言った機能がありませんが、2Dのドット絵なゲームなら十分な機能を持っていると思います。ただし、DarkFunctionEditorをゲーム開発に使うにはちょっと手間が必要です。まず、DarkFunctionEditorが読み取るスプライトシートリスト(xml)を作らなくてはいけません。これはAsepriteのスプライト情報の吐き出し機能を使って、自動生成するPythonスクリプトを書きました。次に、DarkFunctionEditorで作成したアニメーションデータ(xml)を読み込んで表示されるプログラムを実装する必要があります。つまりDarkFunctionEditorのローダーを実装する必要があるわけです。

作業フローとしてはこんな感じでやってました

  • Asepriteでスプライト作成
  • Pythonスクリプトを実行
    • Asepriteのスプライト情報吐き出し機能を使ってスプライト情報を吐き出し(json)
    • 吐き出したスプライト情報をDarkFunctionEditorが読み取る形式(xml)へ変換
  • DarkFunctionEditorでアニメーション情報を作成
  • Pythonスクリプトを実行
    • DarkFunctionEditorで作ったアニメーションデータ(xml)を自分のプログラムで扱いやすい形式(json)へ変換
  • ゲーム内で確認

スプライトアニメーションツールは他にも探したのですが、高機能過ぎたり(高機能すぎるとゲーム側でローダーを実装するのが大変になる)、値段が高かったりで、今回のプロジェクトに一番合っていたのがDarkFunctionEditorでした。

絵素材作成、アイコン作成、企画書作成 : Sketch(Mac)

Sketch(有料) : Mac

Sketchはベクターグラフィックソフトです。グラフィックソフトですが、「センメツコースター」では主に企画書として使いました。テキストエディタと違ってテキストを自由な位置に配置できる上に色や大きさを変えることもできる、枠で囲ったり画像を張り付けたりもできる、1つのファイル内でいくつもページ分けができるなど企画書やアイデア出しに便利でした。他にもiOSアプリアイコンを作成したり*1、このサイトでも利用している「センメツコースター」のバナー画像もSketchで作成しました。99ドルと少し高いですがその価値はあります。ただし、販売モデルが変わってしまい、買い切りではなく1年単位のサブスクリプションとなってしまいました。買い切りモデル時に買った人はアップデートを行わなければそのまま使えますが、アップデートを行ってしまうと即ライセンス切れとなってしまい新たにライセンスを買わないと使えなくなってしまいます。ご注意を*2

音楽作成 : GarageBand(Mac)

GarageBand : Mac

「センメツコースター」ではBGMはPANICPUMPKINさんのフリー素材を使用させていただきましたが、ゲーム成功時と失敗時にジングル(短い音楽)だけどうしてもマッチするものが見つからず、ためしにGarageBandで作ってみたらそれっぽいのができたのでそのまま採用しました。

また、GarageBandファミコン風サウンドを利用するためにMagical 8bit Plugを利用しました。

フォント作成 : bitfontmaker2(Webサービス)

bitfontmaker2 : Webサービス

最初はゲーム中のUI用のテキストをAsepriteで描いて使用箇所ごとにテクスチャ化していました。ところがだんだん面倒になってきたので、フォントを自分で作れないか調べていたところ、このWebサービスを発見しました。ブラウザ上でぽちぽちとドットを打って自分だけのフォントを作ることができます。ちなみに私が作ったフォントはここからダウンロードできます。収録されているのは英大文字小文字数字といくつかの記号だけです。一応英小文字も描きましたがサイズの都合上、見た目が悪いです。

制作サポートツール

バージョン管理 : Git + Source Tree

Source Tree : Mac, Windows

ソフトウェア開発には必須なバージョン管理。ソースコードだけでなくリソース(アセット)含めて全部Gitでバージョン管理してました。クライアントはSource Treeを利用。

バックアップ : Dropbox, Time Machine(Mac)

Dropbox : Mac, Windows
Time Machine : Mac標準搭載(ただし外付けHDDなどが必要)

Dropboxはバックアップのためのツールではありませんが、実質的にバックアップにもなります。ただし、Dropboxが対象としているフォルダのみです。Time MachineはMac標準搭載のバックアップソフトウェアです。システム全体の復旧だけでなく、ファイルを1つだけ選んで復旧なんてこともできます。

クリップボード拡張 : Clipy(Mac)

Clipy : Mac

クリップボード履歴を扱えるクリップボード拡張はコーディングでほぼ必須とも言えるものです。昔はClipMenuを使っていたのですが、頻繁にハングするようになってしまったのでClipyに乗り換えました。

動画キャプチャ : QuickTimePlayer(Mac)

動画キャプチャはゲーム開発の進捗動画や、AppStore用の動画を撮るときに必要になってきます。幸いなことにMacデスクトップの動画キャプチャもiPhone実機の動画キャプチャも無料のQuickTimePlayerで行うことができます。ただしデスクトップの動画キャプチャは標準では音声を撮ることができないので注意が必要です。

動画編集 : iMovie(Mac)

iMovie : Mac

AppStore用の動画は最大30秒までしか許可されていないので、動画を編集する作業が発生します。今回、動画編集ツールとしてはiMovieを利用しました。iMovieは無料な上になんとAppStore用のテンプレートもあります。iMovieは今回初めて使いましたが、そんなに苦労せずに動画を編集することができました。

カラーピッカー : Sip(Mac)

Sip(有料) : Mac

Sipはカラーピッカーです。デスクトップ上の色を見たり、RGB値としてコピーできたりします。カラーピッカーはMacに標準で用意されているものがありますが、色をコピーするのにキーボードショートカットが必要だったりフォーマットを指定できないなど不満があります。Sipは簡単に色をコピーできたり、フォーマットを指定することができます。カラーピッカーを使うことで、ペイントソフトで画面デザインを試行錯誤したあとにそのままカラーピッカーで色をコピーしてソースコードにペーストということができるようになります。ソースコード内で直接色を指定する場面が結構あったので重宝しました。

画像コンバート : XnConvert

XnConvert : Mac, Windows

XnConvertは強力な画像コンバータです。複数のファイルをまとめて、しかも複数の処理を行うことができます。リサイズ、クリッピング、回転、フォーマット変換、やりたいことだいたい用意されています。補間なしでリサイズを行うこともできるのが2Dゲーム開発的にポイント高いです。

パフォーマンス、メモリリーク調査 : Instruments(Mac)

InstrumentsはXcodeに付属するツールでプログラムのパフォーマンス調査ツールです。またメモリリークの調査も行うことができます。ソースコードではなく実際に動いているプログラム(プロセス)にアタッチして使います。Xcodeから起動しなくても単体で起動しますし、Xcodeでビルドしたプログラムでなくてもアタッチすることができます(ただし、デバッグモードでビルドする必要がある…気がします)。「センメツコースター」はPC(Mac)上ではQtCreatorでビルドして動作確認をしていましたが、このQtCreatorでビルドしたプログラムでもアタッチして調査することができました。Instrumentsの仕組みが良くわかっていないので予想でしかないのですが、QtCreatorでのビルドに使っていたコンパイラXcode付属のClangコンパイラだったからかなと思っています。ちなみにInstrumentsはiOS実機のアプリの調査も行うことができます。

汎用スクリプト : Python

Python : Mac, Windows

AsepriteやDarkFunctionEditorで作成したデータをコンバートするのに、Pythonを利用しました。その他いろいろちょっとしたコマンドラインツールとして利用しています。

スクリプト実行ボタン(メニューバー作成) : rumps(Mac)

rumps : Mac

rumpsはMacのデスクトップ上部のメニューバーにメニューを追加できるようにするPythonのライブラリです。Pythonライブラリなので、メニューの追加もPythonコードで書くことになります。「センメツコースター」の開発では、各Asepriteファイルのコンバートや各ステージの直接起動、ステージエディタの起動のためのランチャーとして使っていました。この仕組みを用意することでたいぶ開発効率が上がったと思います。

ブラウザ : Vivaldi

Vivaldi : Mac, Windows

プログラミングをしていると、ネットで検索する必要が必ず出てきます。しかも必要な情報はすぐに得られるとは限らないので大量のタブを開くことになります。前はブラウザとしてFirefoxを使っていましたが、タブを閉じる動作があまりにも遅かったのでVivaldiに乗り換えました。Vivaldiの良いところはタブの動作が高速なことはもちろん、標準でタブの縦並びに対応していることです。さきほど言ったようにプログラミングをしていると調べ物で大量のタブを開くのでタブは縦に並べたいのです。VivaldiChromeと同じ内部エンジンを使っているためChrome拡張が使えたりします*3。最近まで日本語入力バグがありましたが、それも解消され最強のブラウザになりました(個人の感想です)。ただしメモリは大量に喰います。

タスク(Todo)管理 : Trello(Webサービス)

Trello : Webサービス

Trelloはタスク(Todo)管理のためのツール(Webサービス)です。ブラウザ上で動いているとは思えないくらい使いやすいツールです。タスク(カード)をリストに分けたりタグをつけた、詳細説明をmarkdownで書けたりと機能も申し分ないです。さらに複数のボードを利用できるのもポイントが高いです。あまりタスク数が多いと破綻してしまいそうですが、個人〜少人数で使う分には問題なさそうです。Trelloは「センメツコースター」の開発の終盤から使い始めたのですが、もっと早く知りたかったです。

使用ライブラリ

  • OpenGL
  • OpenAL
  • ALURE
  • GLFW(PC上での開発中のみ)
  • Lua
  • PicoJson

使用ライブラリに関しては詳しくはこちらの記事で。

ゲームエンジンを使わずにC++とOpenGLでゲームを作った話 - Flat Leon Works

使用素材

上でフォントを自作したと書きましたがあれは英数字だけなので、ひらがなだけでなく漢字まで含まれているPixelMplusはとても助かりました。音楽はファミコン風の音楽素材を公開してくださっているPANICPUMPKINさんのものを利用しました。やっぱりドット絵にはピコピコなサウンドが良いのです。

最後に

今回紹介した中でとくに良かったのがAsepriteです。Asepriteのスプライト情報出力機能とパック機能のおかげでだいぶ楽できたかなと思います。ところで、「センメツコースター」の開発では今回紹介したツールだけでなく、自分で作ったツールもいくつかありました。その紹介はまた今度ということで。

*1:iOSアプリアイコン作成用のテンプレートも用意されています

*2:私はこのコンボを喰らってSketchをTimeMachineから古いバージョンに復旧しました

*3:使えなかったりもします

プログラミング言語を自作した話

$
0
0

数年前、「Flan」というプログラミング言語を作っていました。このプログラミング言語は長い間C++でプログラミングをしてきて感じた不満をもとに、自分好みの最高のプログラミング言語を作ろうと、そういう考えで作っていました。「Flan」は言語機能的にはだいたい完成していたのですが訳*1あって開発は中断していました。

そして中断から数年経ったわけですが、このまま埋もれさせておくのももったいないなと思い、紹介だけでもすることにしました。公開予定は今のところありません。

サンプルコード

百聞は…ということでまずはソースコード例を紹介。機能を詰め込んだサンプルになっているのでちょっとわかりずらいかもしれません。 また、Pythonコードとして無理やり構文カラーを適用してるので一部変な配色になっています。

このサンプルコードが実際に動作するくらいにはプログラミング言語Flanの開発は進んでました。

## ここはコメント#-ここもコメント
-##- ここもコメント -### 変数
[int]: a <- 1## 変数定義と初期化
[int]: b <- 2print:( a + b ) ## 3print:( a - b ) ## -1print:( a * b ) ## 2    print:( a / b ) ## 0
b = 5## 代入## for文, if文
[list<string>]: strList
strList.Append:( 'aaa' ) ## メンバ関数呼び出し
strList.Append:( 'bbb' )
strList.Append:( 'ccc' )
for i in strList    
    if i == 'aaa'print:( 'A' )
    elif i == 'bbb'print:( 'B' )
    elseprint:( 'X' )
    end
end

## while文,switch文
[int]: i
while i <= 3
    switch( i )
        case 0: assert:( i == 0 )
        case 1: assert:( i == 1 )
        case 2: assert:( i == 2 )
        case 3: assert:( i == 3 )
    end
    i += 1
end

## 関数deffunc:[string]( [int]a, [int]b, [int]c )
    [string]a + b + c ## キャストと暗黙のreturn
end
assert:( func:(123, 456, 789) == '123456789' )

## クラスclassBasedefdump:() ## メンバ関数print:( 'Base.dump' )
    end
end
classA : [Base] ## 継承    classAInner## 内部クラス
    end
    [float]: m_Value ## メンバ変数def@init:( [float]value = 0.0#-デフォルト引数-# ) ## コンストラクタ
        m_Value <- value
    end        
    def@del:() ## デストラクタ
        [stringliteral]: infoStr <- [stringliteral]@typeinfo:() ## 型情報取得print:( '[' + infoStr + '].@del:()' )
    end    
    defdump:() |override| ## オーバーライドprint:( m_Value )            
        if false
            print:( $this.m_Value ) ## this
        end
    end
    defs_func:[int]() |static| ## staticメンバ関数    0
    end        
    deffunc:[int]([int]a,[int],[int]c) ## 引数名の省略(第二引数に注目)print:( 'func: ' + a + $arg1 + c ) ## 引数名を省略した引数へのアクセス
        [self].s_func:() ## [self]で自身の型
    end
    def@unittest:() ## 単体テスト
    end
end

print:( 'staticメンバ関数呼び出し:' + [A].s_func:() )

[A]: a_ins <- 1.23## インスタンス作成
a_ins.dump:() ## 1.230000

[owner:A]: owner_a <- [A].@new:( 4.56 ) ## newでインスタンス作成
owner_a.dump:() ## 4.560000## テンプレート関数deftemplateFunc<T>:([T]param)
    assert:( param == 10 )
end
templateFunc<int>:( 10 )

## テンプレートクラスclasstestTemplate<T>
    [T]: m_ValueT
end
[testTemplate<int>]: tInt
assert:( tInt.m_ValueT == 0 )

## トレイト
trait named_trait
    [string]: name <- 'no name'defgetName:[string]()
        name
    end
    defsetName:([string])
        name = $arg0
    end
end

classCharactor
    has named_trait
end

[Charactor]: charactor
charactor.setName:( 'aaa' )

## 関数オブジェクトdefgetFuncObj:[func0_obj<int>]()
    [int]: outerVariable <- 11returndef:[int]() outerVariable += 1; outerVariable end ## 関数オブジェクトを返す
end

[auto]: funcObj <- getFuncObj:() ## auto型assert:( funcObj:() == 12 ) ## 関数オブジェクトは外部変数をキャプチャしているのでassert:( funcObj:() == 13 ) ## 呼び出すたびに戻り値が変わる## ファイバーclassHoge
    [int]: value <- 10
end
[Hoge]: hoge
[fiber]: fiber <- def:()
                        hoge.value += 1yield## ファイバー中断
                        hoge.value += 1yield
                        hoge.value += 1
                  end
assert:( hoge.value == 10 )
fiber.resume:() ## ファイバー実行assert:( hoge.value == 11 )
fiber.resume:()
assert:( hoge.value == 12 )
fiber.resume:() ## ファイバー実行(そして中断されずに終了)assert:( hoge.value == 13 )
fiber.resume:() ## 終了済みファイバーを実行してもassert:( hoge.value == 13 ) ## ファイバーは終了しているので、もう値は変化しない

プログラミング言語Flanの特徴

ここで紹介する以外にも言語機能はたくさんあるのですが、とりあえず大きめなやつ、または個性的なだけ紹介します。

実行はバイトコードインタプリタ形式

FlanはFlanソースコードバイトコードへ変換し、それをFlanVM(Flanの仮想マシン)が実行することで動作します。Luaと同じ仕組みです。ただし将来的にはC++ソースコード生成による実行も考えています。

静的型付け

FlanはLuaとは違い静的型付けです。つまり、全ての変数には型があり、型が一致しないと(変換不可だと)コンパイル時点でエラーが発生します。

オブジェクト指向

FlanはC++から影響を受けたオブジェクト指向プログラミング言語です。C++に存在する、継承やオーバーライドなどの機能はたいてい言語仕様として含まれています。ただし、多重継承はできません。そのかわりにトレイトがあります。

文末にセミコロン不要

FlanはC++とは違い、文末にセミコロンは不要です。セミコロンを使って1行に複数の文を記述することも可能です。

入れ子可能なコメントアウト

FlanはC++とは違い、コメントアウトを入れ子にすることができます。これはLuaの影響を受けています。

## ここはコメント#-ここもコメント
-##-入れ子
#--なコメントアウトも
--#可能
-##- その場コメント -##- -10 -##!-ここはコメントではない
-#

前方宣言不要

FlanはC++とは違い、前方宣言が不要です。なぜ不要かというと、構文解析(パース)を2パスで行っているからです*2

暗黙のreturn

Flanではreturnを明示的に記述しなくても、関数内で最後の式文が自動的にreturn文になります。

deffunc:[int]([int]a,[int]b)
    a+b ## 最後の文が自動でreturnされる
end

auto型(型推論)あり

Flanでは変数の型としてauto型を使うことができます。これはC++11で導入されたauto型と同じようなもので、初期化の式の型から自動で変数の型を決めることができる機能です。

[auto]: a <- 10
[auto]: b <- 'string'

初期化と代入で違う構文

C++では初期化も代入もどちらも=で行いますが、Flanでは初期化は<-、代入は=で行います。初期化と代入は別の操作なのですから、別の演算子にするべきだと思ったのでこういう仕様にしました。

また、C++では代入は式なのでif文の条件式内で使えたりしましたが、Flanでの代入は文なのでそういうことはできません。

[int]: a <- 10## 初期化
a = 20## 代入

明確な文法

C言語ではこんな書き方ができます。

(int)hoge((1+2)*3);

これをFlanで書くとこうなります。

[int]hoge:((1+2)*3)

C言語では、()の意味がいくつもあります。上の例では、キャストと関数呼び出しと式の優先順序変更がすべて()で行われています。Flanではキャストは[]、関数呼び出しは:()、式の優先順序変更は()とすべて区別されています。

なお、Flanでは[]で囲われた中はすべて型を表します。

交換演算子<=>

変数の中身の交換というのは普遍的な操作なので演算子として存在してもいいのではと思い、交換演算子というものを仕様に入れました。

[int]: a <- 10
[int]: b <- 20
a <=> b
assert:( a == 20 )
assert:( b == 10 )

3種類の参照。所有、共有、弱参照。

Flanには3種類の参照が存在します。1つは所有。これはC++のunique_ptrのようなもので、所有者が1人であることを保証します。この参照が破棄されるとき、参照先も破棄されます。2つめは共有。これはC++のshared_ptrのようなもので、複数の所有者が存在できることを表します。この参照が破棄されるとき、他に所有者がいない場合は破棄されます。3つめは弱参照です。これはC++のweak_ptrのようなもので、所有ではなく"参照"を表します。参照先が破棄されると弱参照はnullとなります。

ソースコードとしては、所有は[owner:Hoge]、共有は[ref:Hoge]、弱参照は[wref:Hoge]という記述方法になります。

すべてが参照ではない

JavaPythonLuaなど多くのプログラミング言語ではプリミティブ型以外のすべてが参照なことが多いですが、Flanは値型と参照型が個別に存在します。

[A]: a ## 値型
[ref:A]: ref_a ## 参照型

参照系演算子

Flanの参照型への操作は、基本的にデリファレンスしてから行われます。つまりC言語でいうところの常に*ptrが行われるということです。例えば参照型変数aと参照型変数bがあったとして、a = bとした場合、abへの参照がコピーされるのではなく、aの参照先にbの参照先が代入されます。C言語で表すと*a = *bです。

では参照をコピーしたい場合にはどうしたらいいのか。それを行えるようにするのが参照系演算子です。例えば参照をコピーしたい場合はa := bとします。=ではなく:=を使うのです。

参照系演算子は他にも参照を交換するための:<=>、参照を比較するための:==などがあります。どれも通常の演算子の前に:が付いているのが特徴です。

classA
    [int]: m_Value
    def@init:( [int]value )
        m_Value <- value
    end
end
[ref:A]: a0 <- [A].@new:( 0 )
[ref:A]: a1 <- [A].@new:( 1 )

a0 = a1 ## 参照先の代入
a1.m_Value = 2assert:( a0.m_Value == 1 )
assert:( a1.m_Value == 2 )

a0 := a1 ## 参照の代入
a0.m_Value = 3assert:( a0.m_Value == 3 )
assert:( a1.m_Value == 3 )

関数オブジェクト(無名関数、クロージャ)

Flanには通常の関数とは別に、値として扱える関数オブジェクトが存在します。この関数オブジェクトは他の言語では無名関数やクロージャとも呼ばれます。Flanの関数オブジェクトはクロージャでもあるので、その関数が定義された環境を保持(キャプチャ)します。

[int]: value <- 11
[auto]: funcObj <- def:[int]() value += 1; value end ## 関数オブジェクトassert:( funcObj:() == 12 )
assert:( funcObj:() == 13 )
assert:( value == 11 ) ## キャプチャはコピーなのでコピー元には影響がない(値型の場合)

引数名の省略(引数名の自動設定)

Flanでは関数の引数名を省略することができます。省略された引数名は$argNのような名前が自動で設定されます。このときNには引数の位置が入ります。

deffunc:[int]([int],[int],[int])
    $arg0 + $arg1 + $arg2
end

引数リストの展開

Flanでは関数内で$argsを使うことで引数リストを展開することができます。これは受け取った引数をそのまま他の関数に渡す場合などに便利です。

deffunc:[int]([int],[int],[int])
    func2:( $args ) ## 引数リストを展開
end
deffunc2:[int]([int],[int],[int])
    $arg0 + $arg1 + $arg2
end

static/非staticで同名のメンバ関数

C++ではstatic/非staticで同じ名前のメンバ関数を作ることができませんでしたが、Flanでは可能です。

classAdeffunc:[stringliteral]()
        'func:()'
    end
    deffunc:[stringliteral]() |static|
        'func:() |static|'
    end
end
[A]: a
assert:( a.func:()   == 'func:()' )
assert:( [A].func:() == 'func:() |static|' )

関数テンプレート

C++のように関数テンプレートが存在します。

deftemplateFunc<T>:([T]param)
    assert:( param == 10 )
end
templateFunc<int>:( 10 )

関数テンプレートのテンプレート引数の推論

これもC++にある機能です。関数への引数から、関数テンプレートのテンプレート引数を推論する機能です。

deftemplateFunc<T,T2=bool,T3>:([T]a,[T3]t3)
    assert:([T].@typeinfo:()  == [int].@typeinfo:() )
    assert:([T2].@typeinfo:() == [bool].@typeinfo:() )
    assert:([T3].@typeinfo:() == [stringliteral].@typeinfo:() )
end

## 本来はこう書く必要があるところを
templateFunc<int,bool,stringliteral>:( 1, '' )

## このようにテンプレート引数を省略して書くことが可能
templateFunc:( 1, '' )

流用テンプレート関数

流用テンプレート関数とは、引数リストを他の関数から流用するテンプレート関数です。オーバーロードされた関数群のラッパー関数を作るのに便利です。

defhoge:[stringliteral]([int]a)
    'hoge:[stringliteral]([int])'
end
defhoge:[stringliteral]([bool]a)
    'hoge:[stringliteral]([bool])'
end
defhoge:[stringliteral]([stringliteral]a)
    'hoge:[stringliteral]([stringliteral])'
end
defhoge_wrapper:[stringliteral](<hoge>) ## <hoge>という記述で流用テンプレートになるprint:( 'Pre Hoge' )
    hoge:($args)
    print:( 'Post Hoge' )
end
hoge_wrapper:( 3 ) ## int型を1つ引数にとるhogeが存在するのでOK
hoge_wrapper:( 5, 6 ) ## int型を2つ引数にとるhogeは存在しないのでエラーになる

内部クラス、内部関数

クラスの内部でクラスを定義することができます。また、関数内部で関数やクラスを定義することも出来ます。

classAclassAInner## 内部クラス
    end
end
deffunc:()
    defInnerFunc:() ## 関数内部関数
    end
    classInnerClass## 関数内部クラス
    end
end

クラステンプレート

C++のようにクラステンプレートが存在します。

classtestTemplate<T>
    [T]: m_ValueT
end

[testTemplate<int>]: tInt
assert:( tInt.m_ValueT == 0 )

トレイト

トレイトとはクラスに機能を持たせるための仕組みです。Wikipediaに記事がありますが、言語機能としてトレイトを持つプログラミング言語でも、その意味は微妙に違っているようです。

トレイト - Wikipedia

乱暴に説明すると、実装を持つインターフェイス(Java)です。

trait named_trait ## 名前トレイト(機能)
    [string]: name <- 'no name'defgetName:[string]()
        name
    end
    defsetName:([string])
        name = $arg0
    end
end

classCharactor
    has named_trait ## キャラクターは名前トレイト(機能)を持つ
end

[Charactor]: charactor
charactor.setName:( 'aaa' ) ## 名前トレイト(機能)のメンバ関数を使える

トレイトテンプレート

トレイトもクラスのようにテンプレートが存在します。

ファイバー(コルーチン)

ファイバー - Wikipedia

ファイバーは中断できる関数オブジェクトのようなものです。Luaにおけるコルーチンとほぼ同じものですが、Fiberの方が文字数が短いのと響きがよいのでFlanではFiberという名称にしました。

classHoge
    [int]: value <- 10
end
[Hoge]: hoge
[fiber]: fiber <- def:()
                        hoge.value += 1yield## ファイバー中断
                        hoge.value += 1yield
                        hoge.value += 1
                  end
assert:( hoge.value == 10 )
fiber.resume:() ## ファイバー実行assert:( hoge.value == 11 )
fiber.resume:()
assert:( hoge.value == 12 )
fiber.resume:() ## ファイバー実行(そして中断されずに終了)assert:( hoge.value == 13 )
fiber.resume:() ## 終了済みファイバーを実行してもassert:( hoge.value == 13 ) ## ファイバーは終了しているので、もう値は変化しない

単体テスト

クラスに@unittestというメンバ関数を定義すると、単体テスト用の関数になります(扱いとしてはstaticメンバ関数)。コンパイル時に単体テストフラグが立っていた場合、実行時にすべての@unittest関数が呼び出されます。

classA
    [int]: m_Value <- 10def@unittest:()
        [A]: a
        assert:( a.m_Value == 10 )
    end
end
classB
    [int]: m_Value
    def@unittest:()
        [B]: b
        assert:( b.m_Value == 0 )
    end
end

## [A].@unittest:() と [B].@unittest:()が自動で呼び出される

FlanIDE

実はプログラミング言語と同時にIDE(統合開発環境)も作っていました。このIDEはQtを使って作りました。下の画像を見ればだいたいわかると思いますが、機能としては以下のようなものを実装しました。一部、実行形式がC++コード生成だったときの名残もあります。

  • コードエディタ
    • 行数
    • 構文カラー
    • エラー箇所に下線
    • カーソル位置の抽象構文木の表示(ウインドウ下部参照)
  • ファイルリスト
  • コードモデル(クラスやメンバ一覧)の表示、ソースコードジャンプ
  • エラーリスト
  • VM(仮想マシン)デバッガー

FlanIDE

FlanIDE

VMデバッガーは最初は実装していなかったのですが、プリントデバッグVMの動作をデバッグするのがとても辛かったので作りました。世のVM開発者の方々はどうやってデバッグをしているのでしょうか…。

プログラミング言語Flanの実装

最後にプログラミング言語Flanをどうやって実装したのかを紹介したいと思います。思い出しながら書いているので間違っている部分があるかもしれません…。また、「言語モデル*3などFlan独特の名称を使ったりしてます。

実行までの流れ

Flanのソースコードから実行までの流れは以下のようになっています。

Flan実行の流れ

パーサー

パーサーはソースコードを受け取り、抽象構文木(AST)を生成します。このパーサーは、パーサージェネレータであるANTLRを使って生成しました。ANTLRはデフォルトではJavaソースコードを生成しますが、C言語コードを生成させることもできます。ちなみに、C言語用のパーサージェネレータは一般的にはlex/yaccが使われるようです。ただし、yaccには抽象構文木の生成機能はありません。

モデルファクトリ

モデルファクトリはパーサーが生成した抽象構文木から、プログラミング言語Flanの言語モデルを構築します。言語モデルとは簡単に言えば、抽象構文木から意味を読み取って新たに構築したデータ構造です。現在の実装ではこの言語モデルバイトコード生成時だけでなくVM実行時にも必要になります。

バイトコードジェネレータ

バイトコードジェネレータは言語モデルを元に、バイトコードを生成します。

FlanVM

FlanVMはバイトコードを実行するための仮想マシンです。FlanVMにはバイトコード言語モデルを与える必要があります。このFlanVMによってバイトコードが実行されることでようやくFlanが実行されたことになります。

プログラミング言語を作るためのステップ

前の節でFlanがどうやってプログラムを実行しているのかを紹介しました。実行の仕組みだけならこれだけで良いのですが、実際にプログラミング言語を作るとなるとより多くの作業が必要になります。その辺を含めたプログラミング言語を作るためのステップを紹介します。

なお、ここでの説明はFlanの場合のもので、必ずしもこの方法が必要というわけではありません。例えばパーサージェネレータを使わずに自分でパーサーを書くこともできます。

文法を決める

プログラミング言語を作るにはまず文法を決める必要があります。さらに文法を決める前にプログラミング言語にどんな機能を持たせる決めなくてはいけません。ここは楽しい場面ですが、機能を追加すればするほどそれを文法に落とし込むのに苦労することになります。文法は最初にすべてを決めるのではなく少しずつ付け足していくことも可能ですが、新しい文法を導入するとすでに決まっていた文法を修正する必要が出てくる場合があります。実装はあとにしても文法だけは最初から考えておいたほうがいいかもしれません。

文法を厳密に定義する

文法が決まったらそれを厳密に定義します。文法を厳密に定義することはソースコードの構造を決めることでもあります。パーサージェネレータは文法の厳密な定義を必要とします。いきなり、パーサージェネレータ用の文法定義を書いてもいいし、BNF記法で一旦書いてからそれをパーサージェネレータ用の文法定義に落とし込んでもいいでしょう。ANTLRの文法定義方法はBNF記法に近いのでいきなりANTLR用の文法定義を書き始めてもあまり困ることはないです。

パーサーを生成する

パーサージェネレータ用の文法定義ができたらパーサーを生成してもらいます。ANTLRは抽象構文木を生成してくれるのでよいのですが、yaccでは自分で抽象構文木を構築するコードを書かないといけないかもしれません。

言語モデルクラスを作成する

言語モデルとはソースコードのデータ構造です。このデータ構造を構築するためのクラス群が必要になってきます。例えばクラス、関数、変数、式、文などを表すクラスです。変数クラス、式クラス、クラスクラスなどを作っていくのはなかなか楽しいかもしれません。言語モデルクラスには各種エラー処理の実装も必要になります。型が一致しない、変数、関数が見つからないなどです。

言語モデルファクトリを作成する

言語モデルクラスができたら、それらを使って言語モデルを構築する言語モデルファクトリを実装します。言語モデルファクトリはパーサーが出力した抽象構文木を走査して言語モデルを構築していきます。

VM(仮想マシン)を作成する

言語モデルの構築までできるようになったら、あとはそれを実行する仕組みを作るだけです。実行するための仕組みとしてFlanではVMを利用しました。VMとは仮想マシンのことでソフトウェアで実装されたCPUのようなものです。CPUは機械語を読み取って動作しますが、VMバイトコードを読み取って動作します。

VMを実装するには以下のようなことをします。

  • バイトコードのフォーマットを考える
    • オペコードのデータサイズ
    • 各種定数格納方法
  • バイトコード実行の仕組みを作る
  • 言語機能の実装に必要な命令セットを考える
  • 命令を実装していく

長くなってしまうので詳細は省きます。「バイトコード」「スタックマシン」当たりで検索してみてください…。

バイトコードジェネレータを作成する

VMが出来たので、そのVMが利用するバイトコードを生成するバイトコードジェネレータを作ります。バイトコードジェネレータは言語モデルを走査してバイトコードを生成していきます。

コンパイラを作成する

ここまでで「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」が出来ました。これら順番に使うことでソースコードからバイトコードを生成することができます。ただ、このままでは不便なのでこれらの機能をまとめた「コンパイラ」を作りましょう。「コンパイラ」はソースコードを受け取り、「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」を順に使い、バイトコードの生成します。

ソースコードを実行する仕組みを用意する

コンパイラVMが出来たのであとは、ソースコードを受け取り、コンパイルし、VMバイトコード(と言語モデル)を渡して実行する仕組みを用意するだけです。

まとめ

プログラミング言語を作るというのは、本当に楽しくて気付いたら1年くらい経過してました。Flanは現在、開発を中断していますがここまで作ったんだからいつか完成まで持っていきたいと思う…ような思わないような。*4

*1:時間をかけすぎてしまったので、本来の目的であるゲーム制作を始めることにした

*2:正確にはちょっと違いますが

*3:今思うと「言語モデル」ではなく「コードモデル」の方が適切だったような気がします

*4:最近Nimというすごく良さげな、C言語ソースコードを生成するタイプのプログラミング言語を見つけて、「これ欲しかったやつだ!」となっています

【Python】言語仕様メモ

$
0
0

ブール値変換

  • 偽になるもの
    • None
    • False
    • 数値ゼロ : 0
    • 空シーケンス : '', (), []
    • マッピング : {}
    • __nonzero__()メソッドがFalseを返すオブジェクト
    • __len__()メソッドが0を返すオブジェクト
  • 真になるもの
    • 偽に当てはまらないもの

ドキュメント: 真理値判定

【Nim】Nimメモ

$
0
0
Viewing all 120 articles
Browse latest View live