データと振る舞いをまとめる手段としてクラスがありますね。
迷った結果、僅差でクラスを使わない方向にしたのですが、エンティティやプレイヤーをクラスにするのも良いと思います。
TL;DR
好きにしてくれ!
継承の話
クラスを知っている人向けの話をするのでクラス自体の説明は省きますが、まず継承ありだとすると、今回の例はこうでしょうか。
class Entity {
constructor(x, y, vx, vy) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
}
updatePosition() {
this.x += this.vx;
this.y += this.vy;
}
}
class Player extends Entity {
constructor() {
super(200, 300, 0, 0);
}
applyGravity() {
this.vy += 0.15;
}
applyJump() {
this.vy = -5;
}
draw() {
square(this.x, this.y, 40);
}
}
この継承というものを使うのがふさわしいケースというのは、クラスで表現している概念(ここではプレイヤーやエンティティ)の関係が綺麗にツリー状になり、その階層が深くなく(深いと理解が困難になるので)、しかもその状態が将来変化することを心配する必要がない場合に限られます。
そして、ゲームのエンティティが綺麗なツリー状になることはあまり期待しないほうが良いと思います。
もし(初学者が混乱しがちと言われる)this
の扱いが分かっているのなら、prototype
のプロパティを直接いじれば多重継承っぽいことができると思いますが、筆者はそういうことをしたことがないのでそれで問題が出ないかはよく分かりません(そのうち確認してみたいです、うまくできるならかなり利用価値があるような気がします)。
では継承に頼らないならどうするかとすると、Java
なら委譲とかコンポジションとか言うところですが、JavaScript
なら普通に関数を値として持たせておくことができる。
Player
クラスの draw()
を例に取ると、
// 名前は適当
function drawSquare(entity) {
square(entity.x, entity.y, 40);
}
class Player extends Entity {
constructor() {
super(200, 300, 0, 0);
this.drawGraphics = drawSquare;
}
draw() {
this.drawGraphics(this);
}
}
もしくは
function drawSquare(entity) {
square(entity.x, entity.y, 40);
}
class Player extends Entity {
constructor() {
super(200, 300, 0, 0);
// bind() で、drawSquare() の引数 entity を this で予約した関数を作る
this.draw = drawSquare.bind(undefined, this);
}
}
ベストかどうかはともかく、これで継承の問題は減ると思います。
prototype
と違ってインスタンス一つ一つに関数を持たせるのは厳密には無駄なのですが、インスタンスの数とプロパティの数を掛け算したらめちゃめちゃ多くなるみたいなケースでもない限りは大きな問題ではありません。
どっちが良いのか
ということで上記のようなコードが一つの選択肢になりますが、単純にデータと関数を並べて都度組み合わせる方法に比べて、このようなコードを書くメリットがどのくらいあるのか?
という話になります(特にこの資料では、読者があまり class
構文に慣れていないことを想定していて、それでもぜひ使おう! と言うべきかどうかという話でもあります)。
クラスのメリット
たとえば情報資産。クラスの説明やクラスを使ったサンプルコードは技術書でもネット上でも溢れていて、いろいろ読み漁るときにクラスに親しんでいたほうが良いというのがあります。p5.js
の場合でも、公式サンプルコードのほとんどが Java ベースの Processing
と互換性を持つように書かれている印象があり、つまりクラスが多用されています(古いコードは ES5 なので
prototype でやってたりしますが、トリッキーなことはせずに Java
のクラスに近いものを想定して使っているはずなので、同じことだと思います)。
先月(2020年1月)刊行されたばかりの書籍『グラフィックスプログラミング入門』
でも、JavaScript
でゲームを作るにあたって、プレイヤーキャラクターや敵キャラクターをクラスで作っていました(話題が逸れますが、ES2015
をベースに基礎をガッチリ説明されているので、しっかり勉強したい人が今読む本としてこの本はお勧めかもしれません)。
それと、コードを読み書きする側のメンタルモデルと一致するかどうかという観点もあります。データと振る舞いがひとまとめになっていたほうがなんだか直感的に分かりやすい気がする、というのは確かにありえて、これもクラスベースの言語が広まった(そして
JavaScript
にも糖衣構文として導入された)理由の一つだと思います。といって、他のタイプの言語をそんなに知っているわけではないのですが。
関数主体のメリット
他方、データと関数だけでなんとかする方法に軍配が上がる要素の一つとしては、ゲームプログラムにおいては大量の似たようなオブジェクトを配列に入れてそれをループ処理することがとても多い、という点があります。
インスタンスにメソッドが生えている形なら
for (const entity of entities) entity.updatePosition();
とするところ、関数主体の場合は
entities.forEach(updatePosition);
みたいなスマートな書き方がやりやすい。
あと個人的なメンタルモデルとしては、エンティティの一つ一つが振る舞いを持っているというより、配列に並んだオブジェクトたちがベルトコンベア式に一つの関数に流し込まれるイメージのほうが最近はしっくりきつつあります(ベルトコンベアの比喩は
Unity の ECS を説明したこの記事を読んでなるほどと思ったのでした、Unity エアプですが)。
そもそもクラス or not なのかというと……
上で書いた、クラスを使いつつ個別のインスタンスに関数を持たせる方法ですが、これはもうほとんど prototype
を活用していないので、単なるオブジェクト形式で良くない? という話にもなりえます。
function drawSquare(entity) {
square(entity.x, entity.y, 40);
}
function createPlayer() {
return {
x: 200,
y: 300,
vx: 0,
vy: 0,
draw: function() {
drawSquare(this);
}
};
}
あるいは
function createPlayer() {
return {
x: 200,
y: 300,
vx: 0,
vy: 0,
draw: function() {
square(this.x, this.y, 40);
}
};
}
つまり、ツリー状の継承を行わない場合 class
構文の使用是非は記法に関する好みの問題に収束し、一方それ以外に残る問題として
- まずオブジェクトがあって、それに関数が結びついている →
player.draw();
- まず関数があって、それにオブジェクトを渡す →
drawSquare(player);
のどちらが良いか、あるいはオブジェクトと関数を結びつける作業をどの時点で行うか、
といった話にシフトするのかもしれません。
で、オブジェクト作成時にそれを関数と結びつける作業を行うよりは、都度自由に組み合わせて実行できるほうが書き方としては素朴に思えたので、そのようにしている次第です。
なお、今はあくまで「プレイヤー」「ブロック」という大きな単位でしか振る舞いの違いが見られず結びつけのパターンが単純であるという前提がありますが、いろんな見た目や挙動の敵キャラが出てくるなどで振る舞いの種類が増えてくる場合には、その部分は事前の結びつけが必要となるでしょう。その結びつけの具体的な方法にしても、種類別にまとめず順不同に実行する必要があるときには一律に
player.draw();
のような形で実行できるようにする必要がありそうですし、そうでなければオブジェクトと関数の対応関係を別で管理する仕組みを作るなど他の方法も考えられるかもしれません。
なんだか、まだまだ検討が足りていないような気がしつつあります。今のところ決着をつける材料を筆者が持っていないので、あとはお好みで……としか言えない状態ではありますが、委譲、じゃなかった以上、ご参考までに……。