JavaScript
ゲーム
UUUMDay 7

JavaScriptで関数型ゲームエンジンを作るよ!part1

はじめに

この記事は、UUUM Advent Calendar 2018 7日目です。
人生紆余曲折あってUUUMに12月に入社した新入社員です優しくしてください。
自鯖のDocker Swarm化を書こうかなーって思ったけど、まとまらなかったのでとりあえずゲームエンジンについて書いていこうと思ってます。

懇切丁寧にブログ記事書くのはめんどくさいので適当な解説しか書いてないし、今後も書かないんじゃないかなぁと思っています。

repoと使用ライブラリ

GitHub: https://github.com/takeokunn/takengine

  • pixi.js
  • stats.js
  • keycode
  • webpack
  • babel
  • eslint

動機

友達「え、エンジニアなのにエンジン作ったことないの????エンジニアなのに????」
僕「とても辛い」
ねねっち「 エ ン ジ ン に 頼 ら ず 一 か ら プ ロ グ ラ ミ ン グ し る ん だ ぁ 〜 」 

Screen Shot 2018-12-06 at 14.44.44.png

僕「ねねっちでもできるならいけるやろ!!!!!」
僕「一応これでも金もらって(※要出典)働いているエンジニアだし、関数型チックに書いてクオリティ高いエンジン作るぞ!!!」

参考にしたもの

今回、参考にしたゲームエンジンは以下のyoutubeの動画です。

Functional Game Engine Design for the Web - Alex Kehayias
https://www.youtube.com/watch?v=TW1ie0pIO_E&t=2s

ClojureScriptで書かれているレポジトリ
alexkehayias/chocolatier

レギュレーション

ただ作るだけでは面白みがないので、縛りを入れました。

ClojureScriptで書かれたalexkehayias/chocolatierを一度もビルドせずに作る

これにより難易度が上がるので楽しさが倍増します。

進捗

・設計思想を固める
・プロジェクトをつくる ← 今回の記事はここまで
・ゲームループをつくる
・初期stateを生成し、ゲームループに合わせて更新する処理を書く
・グラフィックライブラリを導入する
・入力などのイベントを取得する ← イマココ
・当たり判定などのイベントを発火させる
・シーン管理をする
・チャリ走レベルのクソゲーを雑につくる
・シューティングゲームレベルのそれなりのものをつくる
・ゲームを自動でプレイするプログラムを書く
・ゲームを自動でクリアするプログラムを書く

描画して入力に合わせて画像を動かすところまでは動いたヤッター
Screen Shot 2018-12-06 at 16.00.49.png

設計思想

Entity Component Systemをベースに作っています。
オブジェクト指向というよりはデータ指向にコードを書くことができ、美しく書くことが出来て楽しいです。
ECSの説明書こうと筆を進めていたのですが先駆者の記事が素晴らしかったのでめんどくさくなりました。
以下を参考にしたりググってください。

【Unity】Unity 2018のEntity Component System(通称ECS)について(1)
エンティティ・コンポーネント・システム wiki

このプロジェクトではこんな感じでEntity/Component/Systemを定義しています。

Entity周りのコード
// src/game/entity.js
import { player } from 'entities';

export const entity_state = (loader, resources, stage) => ([
    {
        type: 'entity',
        opts: player.create('player1', loader, resources['player'], stage)
    }
]);

// src/entities/player.js
import { pixi } from 'engine_utils';
import { position, renderable, moveable } from 'engine_components';

export const create = (uid, loader, resource, stage) => {
    return {
        uid: uid,
        components: [
            {
                uid: 'sprite',
                state: renderable.mk_sprite_state(loader, stage, resource.name)
            },
            {
                uid: 'position',
                state: position.mk_position_state(20, 20, 0, 0, 0)
            },
            {
                uid: 'controller',
                state: {}
            }
        ]
    }
};

Component周りのコード
import { animate, attack, controller, damage, ephemeral, moveable, position, renderable, text } from 'engine_components';

export const component_state = [
    {
        type: 'component',
        opts: {
            uid: 'position',
            component: {
                fn: position.fn,
                select_systems: [],
                select_components: ['controller'],
                subscriptions: [],
                cleanup_fn: () => {}
            }
        }
    },
    {
        type: 'component',
        opts: {
            uid: 'controller',
            component: {
                fn: controller.react_to_input,
                select_systems: ['key_input'],
                select_components: [],
                subscriptions: [],
                cleanup_fn: () => {}
            }
        }
    },
    {
        type: 'component',
        opts: {
            uid: 'sprite',
            component: {
                fn: renderable.sprite_fn,
                select_systems: [],
                select_components: ['position'],
                subscriptions: [],
                cleanup_fn: () => {}
            }
        }
    },
    {
        type: 'component',
        opts: {
            uid: 'animate',
            component: {
                fn: animate.fn,
                select_systems: [],
                select_components: ['action'],
                subscriptions: [],
                cleanup_fn: () => {}
            }
        }
    },
    {
        type: 'component',
        opts: {
            uid: 'text_sprite',
            component: {
                fn: state => state,
                select_systems: [],
                select_components: ['position', 'text'],
                subscriptions: [],
                cleanup_fn: () => {}
            }
        }
    },
    {
        type: 'component',
        opts: {
            uid: 'movement',
            component: {
                fn: state => state,
                select_systems: [],
                select_components: [],
                subscriptions: ['move_change', 'collision'],
                cleanup_fn: () => {}
            }
        }
    },
];
system周りのコード
import { audio, collision, event, input, meta, renderer, replay, tiles } from 'engine_systems';

export const system_state = [
    {
        type: 'system',
        opts: {
            uid: 'events',
            fn: event.system
        }
    },
    {
        type: 'system',
        opts: {
            uid: 'key_input',
            fn: input.system
        }
    },
    {
        type: 'system',
        opts: {
            uid: 'meta',
            fn: meta.system
        }
    },
    {
        type: 'system',
        opts: {
            uid: 'tiles',
            fn: tiles.system
        }
    },
    {
        type: 'system',
        opts: {
            uid: 'render',
            fn: renderer.system
        }
    },
    {
        type: 'system',
        opts: {
            uid: 'audio',
            fn: audio.system
        }
    }
];

こんな風にEntity/Component/Sysytemを定義し、State化してgame loopで回してごにょごにょするとそれっぽい動きをしてくれます。

プロジェクトをつくる

2018年12月6日現在
.
├── LICENSE
├── README.md
├── package.json
├── public
│   ├── bundle.js
│   ├── img
│   │   └── icon.png
│   └── index.html
├── src
│   ├── engine
│   │   ├── components
│   │   │   ├── animate.js
│   │   │   ├── attack.js
│   │   │   ├── controller.js
│   │   │   ├── damage.js
│   │   │   ├── ephemeral.js
│   │   │   ├── index.js
│   │   │   ├── moveable.js
│   │   │   ├── position.js
│   │   │   ├── renderable.js
│   │   │   └── text.js
│   │   ├── core
│   │   │   ├── core.js
│   │   │   ├── ecs
│   │   │   │   ├── get.js
│   │   │   │   ├── index.js
│   │   │   │   └── make.js
│   │   │   ├── events.js
│   │   │   └── index.js
│   │   ├── systems
│   │   │   ├── audio.js
│   │   │   ├── collision.js
│   │   │   ├── event.js
│   │   │   ├── index.js
│   │   │   ├── input.js
│   │   │   ├── meta.js
│   │   │   ├── renderer.js
│   │   │   ├── replay.js
│   │   │   └── tiles.js
│   │   └── utils
│   │       ├── index.js
│   │       ├── pixi.js
│   │       └── stats.js
│   ├── entities
│   │   ├── index.js
│   │   └── player.js
│   ├── game
│   │   ├── component.js
│   │   ├── entity.js
│   │   ├── index.js
│   │   ├── renderer.js
│   │   ├── scene.js
│   │   ├── system.js
│   │   └── tilemap.js
│   ├── main.js
│   └── manifest.json
└── webpack.config.js

こんな感じでDirectory/Fileを作りました。

Webpack でjavascriptをbundleしてpublicに吐きだし、 webpack-serve でサーバーを立ち上げています。

今回はここまで

読んだことも書いたこともないClojureScriptを解読するの、技術的体力がつきますね。
ClojureDocs 死ぬほど読みやすいので他の言語も参考にしてほしいです。

UUUMでエンジニアをするとヒルズから見える絶景を堪能できます!
仕事でゲームエンジンは開発しないけどね
詳しくはこちら →→→→→→ UUUM攻殻機動隊の紹介