Hatena::ブログ(Diary)

土屋つかさのテクノロジーは今か無しか

2016-12-01

オリジナルのメッセージ指向ゲーム開発言語「司エンジン」でジャンプアクションゲームを作ってみる

・「ゲームエンジンライブラリツールの開発 Advent Calendar 2016(http://qiita.com/advent-calendar/2016/gameengine)」の2日目です。1日目はNumAniCloudさんによる「コレクションの列挙中に安全にコレクションを更新するノウハウ」でした。

はじめに

・はじめまして、土屋つかさと申します。今回は、土屋が個人で製作しているオリジナルのゲーム開発に特化して設計したプログラミング言語 司エンジン を紹介し、実際にジャンプアクションゲームを作ってみたいと思います。コードは全体で130行程度になります(最後にリンク先を示します)。

司エンジンとは

・司エンジンは「メッセージ指向」が特徴なゲームを開発用のプログラミング言語です。単一のツリーに登録された全てのオブジェクトが並列に動作するステートマシンとして機能し、オブジェクトからオブジェクトへ「プログラム群を送信する」ことでゲームを制御します。
・司エンジンは土屋つかさオープンソースプロダクトとして個人で開発していて、現在はRubyの内部DSLとして処理系を実装しています。基幹ライブラリにはDXRubyを使用しており、現状はWindows専用になります。
・司エンジンの概念については下記記事も参照してください。この記事では実際に司エンジン上で動作するプログラムを作っていきます。
そろそろゲームプログラミング用語を共有していこうじゃないか(あるいは既存ゲームエンジンアーキテクチャに対する司エンジンの優位性について)
http://d.hatena.ne.jp/t_tutiya/20161201/1480597742

デモ動画

・最終的に出来上がるデモはこんな感じになります。
https://youtu.be/RQ_9wLGY4A4
D

自キャラ表示

・まずは自キャラを表示しましょう。司エンジンでは画面に表示する各オブジェクトを「コントロール」と呼びます。自キャラを表示する為にコントロールを生成してみましょう。

#キャラの生成
_CREATE_ :Image, id: :main_char, width:32, height:32, color: C_RED, x:32, y:416, z:100

・司エンジンでは「コマンド」が処理単位になります。条件分岐や繰り返し処理もコマンドです。
・コントロールの生成には_CREATE_コマンドを使用します。標準で用意されているコマンドはアルファベット大文字の前後にアンダーラインが付いています。これはruby予約語と被らないようにしているだけで、それ以上の意味はありません。
・Imageは画像を管理するコントロールです。通常は画像へのファイルパスを指定しますが、ここでは32x32ドット赤単色の画像を生成し、:main_charという名前をつけて[32, 416]に配置しています。
f:id:t_tutiya:20161130232336p:image

マップの表示

次はマップを表示します。

#マップデータ
MAP = [ [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1],
        [1, 0, 0, 1, 1, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1],
        [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1],
        [1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1],
        [1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1],
        [1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1]]

・定数MAPに16×16の配列を格納します。司エンジンはrubyの内部DSLなので、この定数のようにruby記法を使うこともできます。要素はそれぞれ0=空(通れる)/1=壁(通れない)/2=雲(通れない)を表します。この配列はマップの表示とキャラの当たり判定に使用します。

#マップの表示
_CREATE_ :TileMap,
  width: 1024, height: 1024, size_x: 16, size_y:16, map_array: MAP do
  _SET_TILE_ 0, path: "./script/jump_action/blue.png"
  _SET_TILE_ 1, path: "./script/jump_action/blown.png"
  _SET_TILE_ 2, path: "./script/jump_action/white.png"
end

・ここではTileMapコントロールを生成しています。このコントロールはRPGツクールのような2Dマップを表示するのを支援する物で、ここでは1024x1024のサイズのTilrMapを表示しています。
・_CREATE_コマンドにブロック(do〜end)が付与されている場合、コントロールの生成後にブロック内に記述されているコマンドを実行します。ここでは_SET_TILE_コマンドを使って画像を読みこみ、それぞれidに対応させます。
f:id:t_tutiya:20161130232334p:image

コマンドの送信

・それではいよいよ自キャラを動かしてみます。司エンジンではすべてのコントロールはステートマシンであり、個々のコントロールにコマンドを送信するとことで挙動を制御できます。

#自キャラにコマンドを送信する
_SEND_ :main_char do
  #プロパティを動的に生成
  _DEFINE_PROPERTY_  y_prev: 0, f: 1

  state_normal #通常ステートに遷移
end

・_SEND_は指定したコントロールにコマンドを送信します。ここではmain_charを送信先に指定し、2つのプロパティ(y_prev/f)を追加して初期値を設定する_DEFINE_PROPERTY_コマンドと、ユーザー定義コマンドであるstate_normalコマンドを送信します。
・この「送信」というのは文字通りの意味で、_send_のブロック内に記述されたコマンド群は、送信先コントロールのキューに動的にスタックされ、そのコントロールの処理タイミングに実行されます。
・司エンジンではループや条件分岐もコマンドとして実装されているので、プログラム構造自体を送信することができます。これが「メッセージ指向」たる所以です。

通常ステート

それではnormal_stateの中身を見ていきましょう。

#通常ステート
_DEFINE_ :state_normal do

  #スペースキー入力判定
  _CHECK_INPUT_ key_push: K_SPACE do
    _RETURN_ do
      sate_jump #ジャンプステートに遷移
      state_fall #落下ステートに遷移
    end
  end

  #フレームを終了する
  _END_FRAME_

  _RETURN_ do
    state_x_move #X方向移動ステートに遷移
    state_fall #落下ステートに遷移
  end
end

・_DEFINE_はユーザー定義コマンドを作るコマンドです。ここではグローバルで宣言してますが、コントロール内で宣言することもできます。
・このサンプルでは、自キャラの挙動をステート(状態)という単位で管理しています。normal_stateはジャンプボタンの入力を受け付けるステートになります。中身を見ていきましょう。

  #スペースキー入力判定
  _CHECK_INPUT_ key_push: K_SPACE do
    _RETURN_ do
      sate_jump #ジャンプ開始ステートに遷移
      state_fall #落下ステートに遷移
    end
  end

  #フレームを終了する
  _END_FRAME_

・_CHECK_INPUT_はキー入力判定を行い、指定したキーが押されている場合にブロックを実行します。ここではスペースキーの押下を判定し、押されていたらジャンプ開始ステートに遷移します。
・_END_FRAME_を実行するとコントロールは次フレームまで待機します。全てのコントロールは並列動作しています。

  _RETURN_ do
    state_x_move #X方向移動ステートに遷移
    state_fall #落下ステートに遷移
  end

・_RETURN_はユーザー定義コマンドを抜けるコマンドです(何度も書いてますが司エンジンではreturnもコマンドです)。
・_RETURN_コマンドにブロックが付与されている場合、ユーザー定義コマンドを抜けた直後にその中身を実行します。これによってステートの遷移が可能になります。複数のステートを並べておけば連続して実行することができます。この仕組みによって、司エンジンでは状態遷移を極めて容易に記述できます。

X方向移動ステート

#X方向移動ステート
_DEFINE_ :state_x_move do
  #横キー入力判定
  _PAD_ARROW_ 0 do |x:, y:|
    _GET_ [[:x,,:now_x]] do |now_x:|
      #X方向の増分を加算
      _SET_ x: now_x + x * 4
    end
  end

・X方向移動ステートでは自キャラの左右移動を行います。_PAD_ARROW_コマンドでは十字キーの押下状態を[-1, 0, 1]で取得します。
・_GET_はそのコントロールが保持しているプロパティの値を取得します(他のコントロールを指定してそこからプロパティを取得することもできます)。ここでは名前の衝突を避けるためにxをnow_xという名前で取得します。
・取得したnow_xにX方向増分に係数を掛けた物を足して、_SET_コマンドで再格納します。これによってX座標が更新されるので、次フレームで自キャラの位置が更新されます。

  #壁衝突判定
  _GET_ [:x, :y] do |x:, y:|
    #壁衝突判定(左側)
    if MAP[y/32][x/32] == 1 or MAP[(y+31)/32][x/32] == 1
      _SET_ x: x/32*32 + 32
    #壁衝突判定(右側)
    elsif MAP[y/32][(x+31)/32] == 1 or MAP[(y+31)/32][(x+31)/32] == 1 
      _SET_ x: x/32*32
    end
  end
end

・自キャラの座標とMAP配列の該当位置を比較して、壁にぶつかっていたら座標を補正します。

ジャンプ開始ステート

#ジャンプ開始ステート
_DEFINE_ :sate_jump do
  #ジャンプ係数を初期化
  _SET_ f: -15

  _GET_ :y do |y:|
    #前フレームのY座標を保存
    _SET_ y_prev: y
  end
end

・ジャンプ開始ステートは、ジャンプボタン押下直後に1回だけ実行されるステートです。ここではジャンプ処理の準備としてジャンプ係数(f)と前フレームのY座標(y_prev)を設定します。

落下ステート

・最後は落下ステートです。落下だけでなく、ジャンプボタン押下直後からの放物線移動もここで処理しています。

#落下ステート
_DEFINE_ :state_fall do
  _GET_ [:f, :y_prev, :y] do |f:, y_prev:, y:|
    #前フレームのY座標を保存&ジャンプ係数の初期化
    _SET_ f: 1, y_prev: y
    #Y軸移動増分の設定
    y_move = (y - y_prev) + f
    #座標増分を加算。増分が31を越えていれば強制的に31とする
    y += y_move <= 31 ? y_move : 31
    #Y座標の更新
    _SET_ y: y
  end

・Y座標の増分を制御します。ジャンプと落下を同時に処理しているのでアルゴリズムがわかりにくいのですが、興味のある方は後述する参考記事を読んでみてください。

  #マップ外落下判定
  _CHECK_ over: {y:480} do
    _SET_ x: 32, y: 0, y_prev: 0
  end

・_CHECK_はプロパティの値を判定するコマンドです。ここではyが480以上(over)であれば自キャラの座標をリセットし、画面外落下とリポップを実行します。

  _GET_ [:x, :y] do |x:, y:|
    #床衝突判定
    if MAP[(y+31)/32][x/32] == 1 or MAP[(y+31)/32][(x+31)/32] == 1
      #Y座標補正
      _SET_ y: y/32*32

      _RETURN_ do
        state_normal #通常ステートに遷移
      end
    end

    #天井衝突補正
    if MAP[y/32][x/32] == 1 or MAP[y/32][(x+31)/32] == 1
      _SET_ y: y/32*32 + 32
    end
  end

  #フレームを終了する
  _END_FRAME_

・床と天井の衝突を判定し、Y座標を補正します。ここで使っているif文はrubyの構文であり、司エンジンのコマンドではないので注意してください。司エンジンはrubyの内部DSLなのでこういう書き方も可能です。
・床に衝突した場合には、そこで落下ステートを終了して通常ステートに戻ります。

  _RETURN_ do
    state_x_move #X方向移動ステートに遷移
    state_fall #落下ステートに再遷移
  end
end

・床に衝突していない場合、X方向移動ステートに遷移した後、落下ステートに再度遷移します。

完成!

・以上で完成です。コードはgistに置いておきます(司エンジンv2.1対応版のコードです。v2.1はクリスマスに正式リリース予定です)。
https://gist.github.com/t-tutiya/c5307db19ff562d3c16ae49cdb6cc21e
・今回は動くコントロールが自キャラだけでしたが、司エンジンでは全てのコントロールが並列で自律動作するので、それぞれのコントロールにコマンドを送信することで、個々のコントロールを簡単に非同期的な挙動を取らせることができます。

司エンジンの他のサンプル

・司エンジンは汎用のゲーム開発用プログラミング言語ですが、特に文字描画を得意としています。以下は制作中の二次創作ゲームのデモです。複数のテキストウィンドウが並列に動作している点にご注目ください。
D
・他にもブロック落としやADVなども作られています。
f:id:t_tutiya:20161130232329p:image
f:id:t_tutiya:20161130232327p:image

司エンジンの入手方法

・司エンジンの実装一式とリファレンスマニュアルは以下からダウンロードできます。動作するサンプルコードも同梱されているので、是非動かしてみてください。
メッセージ指向ゲーム開発言語「司エンジン」v2.0をリリースしました
http://d.hatena.ne.jp/t_tutiya/20160828/1472364019

終わりに

・ここまでお読み頂きありがとうございました。いかがでしたか? 司エンジンはまだ完成したばかりで知名度が限りなくゼロに近いのですが、ゲームの作りやすさには自信があるので、今後広められればと思っています。
・3日目はswdさんの「複数プログラミング言語対応ゲームエンジン「Altseed」の紹介」です。

参考リンク

・今回の記事は、以前書いたこちらの記事をベースに作成しています。キャラと壁の当たり判定についても詳しく解説していますので参考にどうぞ。
2Dアクションゲームの簡易衝突判定入門
http://d.hatena.ne.jp/t_tutiya/20131211/1386774368

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証