セットアップと描画
p5.js (または q5.js) の setup
関数内のコードは、プログラムの開始とともに実行されます。new Canvas()
コンストラクタは、スクリーン内にプログラムが描画できる領域を作成します。
p5.js の draw
関数は、デフォルトでは 1 秒間に 60 回実行されます。background
関数は、キャンバスが描画されるたびに、それを指定した色で塗りつぶすために使用します。
コードサンプルで、キャンバスの幅と高さ(Canvas
コンストラクタ内の数字)を変更して、プログラムを再実行してみましょう!
スプライトって何?
スプライトとは、妖精という意味の言葉です!
ビデオゲーム開発の世界では、背景の上を動くキャラクターやアイテムなどの表示物を指して「スプライト」という言葉を使います。
new Sprite()
コンストラクタで、スプライトのオブジェクトを生成します。これにはスプライトの位置、サイズ、および外観を定義するプロパティが含まれます。
下のコードサンプルで、長方形と円のスプライトのプロパティを編集してみてください!
やってみよう!
次のコードサンプルで、 ball
と名付けられたスプライトを、を直径 30 の青い円に変えて、キャンバスの右上の角に配置してみてください。
スプライトの物理挙動
スプライトのコライダー(衝突を判定する仕組み)は、
他のスプライトとのコリジョン(衝突)を検出するために使用されます。
スプライトがデフォルトで持つ 'dynamic'
コライダーは、
自由に移動でき、重力の影響を受けられるコライダーです。
'static'
コライダーを持つスプライトは、動かせません。
'kinematic'
コライダーを持つスプライトは、プログラム的に移動させることができますが、他のスプライトの影響を受けません。また、他の kinematic
コライダーともコリジョンを起こしません。
スプライトのコライダーを 'none'
に設定すると、コライダーを持っていないことになります。
コライダーのタイプは、 'd'
、 's'
、 'k'
および 'n'
のように、それぞれのタイプ名の頭文字で指定することもできます。
コードサンプルを再生するには、右上のリロードアイコンをクリックしてください!
やってみよう!
peg
という名前で、static コライダーを持つ円形のスプライトを作成します。次に block
という名前で、 dynamic
コライダーを持つ四角のスプライトを作成します。 peg にぶつかって右側に落っこちるように、 block を配置してみてください。
p5.js の draw
関数が実行されるたび、スプライトは自動的に描画・更新されます。 world
は p5play
がロードされるときに生成されますが、デフォルトでは重力は設定されません。 world.gravity.y
を正の数に設定してみましょう。
さらに挑戦したい方は、ブロックが落ちた後に元の位置に戻すようにしてみてください。
画像付きスプライト
sprite.image
または sprite.img
に、p5.Image 画像オブジェクトそのもの、または画像ファイルへの URL
パスを設定することで、スプライトに指定した画像を割り当てることができます。
画像の読み込みが完了してからプログラムを開始したい、という場合は、 p5.js の preload
関数で loadImage
を使用するのがベストです。
sprite.image.offset
は、画像をスプライトの中心に対してオフセットさせるために使用できます。これにより、画像をスプライトの物理的な衝突判定とよりよく整列させることができます。
sprite.image.scale
は、スプライトの画像の表示サイズを変更します。デフォルトは 1.0
です。画像が非常に大きすぎるか小さすぎる場合は、おそらく画像ファイル自体のサイズを変更する必要があります。
コードサンプルの画像の上で、マウスの左ボタンを押してみてください。 sprite.debug
プロパティが true
に設定されると、スプライトのコライダー部分が見えるようになります。これで分かる通り、コライダーの大きさと画像の大きさは、別々に設定可能なのです!
絵文字スプライト
ダミー用の画像がお手元にない? 🫥 ご安心ください! 😄
お好きな絵文字フォントを、スプライトの画像として使用できます。
画像としての絵文字のサイズは、スプライトのサイズに基づきます。サクッとプロトタイピングを済ませるのに最適です! 🧪
ピクセルアート
spriteArt
関数で、スプライト用のピクセルアート画像を作成できます。この関数は文字列を引数として受け取り、画像を返します。文字列内の各文字は、画像内のピクセルの色を表現しています。
spriteArt
関数の第 2 引数は、画像のスケール値(倍率)です。
やってみよう!
コードサンプルで、あなたオリジナルのピクセルアートを作ってみましょう! 下のアルファベットを参考にすれば、各アルファベットにはデフォルトでどの色が割り当てられているかを確認できます。
カスタムカラー
さらに、「カラーパレット」を作成して、spriteArt
関数への第 3 引数に渡すことで、カスタムカラーを使ったピクセルアートを作成することもできます。
p5play のカラーパレットはJavaScript
オブジェクト形式でなければなりません。シンプルな JS
オブジェクトとは、見出しと値のペアの集まりです。ピクセルアートで使用するそれぞれの文字に対し、任意の色を定義することができます。色を作成するには、p5.js のcolor
関数に、
RGB(赤、緑、青)値、または HEX カラーコードを引数として渡します。
色の指定値を調べるには、カラーピッカーを使うのがいちばんお手軽です。
スプライトの移動
スプライトを移動させようとして、座標 (x, y) を直接編集すると、即座に指定した座標へと「テレポート」します。移動の途中経過の座標を示すことはありません。
このコードサンプルをクリックしてみてください。
スプライトの移動中に、他のスプライトと物理的に干渉させたい場合は、「テレポート」を行ってはいけません!
このコードサンプルは、悪い例です。 p5.js の draw 関数によるフレーム毎のレンダリングを利用して、移動をスプライトの「テレポート」で表現すると、どうなるのを示しています。
このページの、以降すべての説明は、移動の方法としてスプライトの x 軸と y 軸の移動量 velocity
(ベロシティ)、別名 vel
を変更することによって行っています。
vel
は p5.js の Vector オブジェクトですが、任意のベクトル関数を使用可能です。
このコードサンプルを再実行して、プレイヤーのスプライトがブロックにぶつかる様子をご覧ください!
場合によっては、スプライトの移動には、 direction
(方向)と speed
(速度)を設定するほうが便利かもしれません。
方向は、角度を指定するか、 'up', 'down', 'left', 'right', 'upLeft', 'upRight', 'downLeft', 'downRight' といった方向名を使って設定できます。
move
関数は、スプライトの移動を、「現在の位置からの距離」で指定します。方向と速度は、関数の引数として指定することも、前の例のように別々に設定することもできます。
moveTowards
関数は、スプライトを特定の位置に向けて、現在の位置との距離とのパーセントぶん移動させます。
このコードサンプルでは、プレイヤーは p5.js の draw 呼び出し毎に、マウスまでの距離の 10%ぶん移動します。そのスピードと、ブロックを弾き飛ばす力は、移動した距離に比例します。
moveTo
関数はスプライトを、一定の速度で特定の位置に移動させる「推進力」(インパルス)を発生させることで、移動させます。
しかし、移動中にそのスプライトが、重力などの外力の影響を受けたり、他のスプライトにぶつかるなどすると、その影響によって指定した位置に到達しない可能性があることに注意してください。
「x と y をプロパティとして持つオブジェクト」を引数にするすべての移動関数は、 (x, y) の座標数値を使用して呼び出すこともできます。
このページで示した例が、 p5play で利用可能な「スプライトの移動手段」のいくつかを理解する助けになったことを願っています!
ただし、 move
、 moveTo
、および moveTowards
関数は、スプライトの現在の動きを強制的に上書きし、新たな方向に動かす、ということには気をつけてください。このことは、必ずしも望ましい結果を生むとは限らないためです!
重力など、外力を考慮に入れたスプライト移動の方法を学ぶには、「高度な移動」を参照してください。
スプライトを手早く作る
new Sprite()
コンストラクタの引数で、スプライトの位置、サイズ、コライダーのタイプを直接指定できます。
これまでの説明ページで見た通り、スプライトを生成するだけなら、 Sprite コンストラクタに引数を与える必要はありません。しかし、コンストラクタ引数でスプライトのサイズを与えたい場合は、あらかじめその座標を指定する必要があります。
デフォルトでは、Sprite コンストラクタが空の場合、生成されるスプライトは、位置がキャンバスの中央、幅と高さが 50 ピクセルで、 dynamic コライダーを持ちます。
やってみよう!
new Sprite()
コンストラクタに引数を与えることで、 2 つのスプライトを作成してみてください。
コリジョン
コリジョン判定には、 p5.js の draw
関数の中で、以下に示す関数を使用します。
スプライトが別のスプライトとのコリジョンを発生させた最初のフレームでは、collides
関数は true を返します。
スプライトが別のスプライトとコリジョンを起こしている間、colliding
関数はコリジョン(衝突あるいは接触)が発生しているフレーム数を返します。
2 つのスプライトのコリジョンが終了した最初のフレームでは、collided
関数は true を返します。
オーバーラップ
スプライトはデフォルトではコリジョンを起こしますが、重なる(オーバーラップ)こともできます!
デフォルトでは、スプライトは生成された順番に描画されます。
レイヤー
描画の順序を変更するには、スプライトの .layer
プロパティを編集します。 layer の値が高いスプライトが手前に、低いスプライトはその後ろに、順に描画されます。
オーバーラップ判定には、 p5.js の draw
関数の中で、以下に示す関数を使用します。
スプライト同士のオーバーラップが発生した最初のフレームでは、overlaps
関数は true を返します。
スプライト同士のオーバーラップが継続している間、overlapping
関数は継続しているフレーム数を返します。
スプライト同士のオーバーラップが終了した最初のフレームでは、overlapped
関数は true を返します。
スプライト間の物理挙動(コリジョンやオーバーラップを含む)は、スプライトが「テレポート」されると正しく検出されないことに注意してください。 スプライトが「テレポート」されると、その座標が直接変更されるためです!
remove
関数で、そのスプライトを削除します。
やってみよう!
青いスプライトが赤いスプライトとオーバーラップしている間だけ、赤に変わるようにしてみてください。
オーバーラップとコリジョンとを切り替える
2つのスプライト間にオーバーラップ関係を設定すると、それらはコリジョンを起こさなくなりますが、 collides
関数を使うことで、再びコリジョン関係とする事ができます。
このコードサンプルでは、スペースキーを押している間だけ、プレイヤーは壁とオーバーラップ関係になり、壁をすり抜けることができます。
ローテーション
スプライトの rotation
(回転角:ローテーション)プロパティを直接変更すると、指定された回転角へ「テレポート」します。
移動における「座標のテレポート」同様、回転による他のスプライトとの物理挙動を望むなら、回転角の直接変更、つまり「回転角のテレポート」は厳禁です!
このページの他のすべての回転メソッドは、スプライトの rotationSpeed
を変更することで行なっています。
rotate
関数を使用すると、スプライトの向きを、その時点での角度から、第 1 引数で指定した角度ぶん回転させます(正数は右回りへの、負数は左回りへの角度を指定します)。
第 2 引数を指定することで、スプライトがフレーム(画面の更新頻度。デフォルトでは 1/60 秒)ごとに何度回転するのか、つまり回る速さを変更できます。
rotateTo
関数を使用すると、スプライトの向きを、その時点での角度にかかわらず、指定した角度に回転させます(真上を 0
度とした、右回りの角度で指定します。負数も指定可能で、その場合は左回りの角度指定となります)。 rotateMinTo
関数は、スプライトが最小の動きで目標に正対するよう、必要に応じて回転方向も変更します。第 3 引数を与えた場合、目標に向かってどのくらいの角度で「正対する ("facing") 」のかを意味します(
0 で真っ正面になります)。
試しに、このコードサンプルを使って、「正対する」角度を 0 から 90 に変更してみましょう。変更前にサンプルをクリックすると、長方形はその短辺をマウスカーソルに向けていましたが、変更後には長辺を向けるようになります。
rotateTowards
関数は、スプライトの向きを、第 1 引数に応じて変更します。第 1 引数が数値であればその角度、位置オブジェクトならそれに正対する角度です。
第 2 引数を指定することで、「向き直る速さ」を変更できます。指定方法は「各フレーム時点で、スプライトが目指す角度との現在の角度との差の、百分率(パーセント)」です。デフォルトでは 0.1(10%)です。
offset
プロパティの x
および y
値を編集すると、スプライトのコライダーを、スプライトの中心点から指定した量だけ「ずらす」事ができます。
sprite.debug
が true のとき、緑色の線でスプライトのコライダーが、小さな緑色の十字線でスプライトの中心点が、それぞれ表示されます。中心点とは、スプライトの
x, y 座標です。また中心点は、回転軸でもあります。
スケーリング
sprite.scale
を変更すると、指定された倍率でスプライトのコライダーと視覚的な外観がスケーリング(拡大/縮小)されます。
コードサンプル上で数字キーを押すと、スプライトがその量だけ均一にスケールします。
マウスクリックまたはタップで、スプライトのスケールが倍になります。
"x" キーまたは "y" キーを押すと、スプライトがその方向にランダムな量だけスケールします。ただし、スプライトが不均等にスケーリングされた場合、画像は歪んでしまい、その後で均一なスケーリングを試みても、歪みはそのままです。
物理挙動の属性
スプライトには、ワールドとの相互作用に影響を与える物理挙動の属性があります。これらの属性がどのように作用するかは、このページのコードサンプル群をご覧ください。
質量
デフォルトでは、mass
(質量)はスプライトのサイズが大きいほど大きくなります。スプライトのサイズが最初に定義された時点で、その質量が計算されます。
しかし現実世界では、物体を引き伸ばそうが押しつぶそうが、質量が変わることはありません。なので、 p5play
ではスプライトのサイズをあとから変更しても、質量は変わりません。スプライトのサイズを変更した後に質量を再計算するには、 resetMass
関数を使います。
やってみよう!
このコードサンプルは、異なる大きさのスプライトが、質量を同じ値に設定した場合、シーソーの上でどのようにバランスをとるかを示しています。では、スプライトの中から一つだけ、質量を変更してみてください。
planck のバグ
p5play は、 Box2D 物理エンジンの JS 移植版である planck.js を使用しています。このエンジンはリアルなインタラクションを高いパフォーマンスで実現しますが、注意すべき制限もあります。
このコードサンプルでは、ボールの bounciness
(跳ね返り係数)が 1 なので、ボールがバウンドするたびに、それはその開始位置に戻るはずです。しかし、planck
のバグにより、ボールは地面で弾むたびに跳ね返りが増加し、しだいに高く跳ねるようになります。
この完全反発の挙動は、現実世界では起こり得ませんが、ビデオゲームでは重要な要素であるはずです。
しかしこの bounciness
のバグは、さきの例のように、物体が地面などのフラットな面からバウンドするときに特に目立ってしまいます。
回避策として、落下したボールが地面とコリジョンを起こした直後に、 y 方向のベロシティを上書きする方法をご紹介します。
次のコードサンプルでは、ブロックと動く床がコリジョンを発生させたとき、ブロックに赤く色をつけるようにしています。床によってブロックが持ち上げられている間のコリジョンは、感覚的には衝突ではなく「接触」なのですから、ブロックが赤色のままであることを期待するでしょう。しかしご覧のとおり、ブロックは赤と青で点滅するのです。
現実世界では、人がエレベーターに乗って上昇すると、その人とエレベーターの床とのコリジョンは「接触」であるとみなすはずです。
しかし、 planck では、コライダーが他のコライダーによって移動させられたとき、それらは「接触の継続」ではなく、「コリジョンの発生」と「コリジョンの終了」が激しく繰り返されているとみなされます。
もしプラットフォーマーゲームを作る場合、スプライトがプラットフォームに立っているかどうかの判定に colliding
関数を使うのはおすすめできません。より良い方法として、私たちのプラットフォーマーデモのコードをチェックしてみてください。
高度な移動
move
関数は強制的に、スプライトの速度を上書きします。しかし、スプライトが重力などの外力を考慮する必要がある場合はどうすれば良いでしょうか?
そのためのプロパティが、 bearing
(方位角:ベアリング)です。ベアリングとは、「特定の位置に到達するために、力をかけるべき角度」という意味の言葉です。
しかし、スプライトの bearing
を変更しても、即座に移動方向が変更されるわけではありません。 applyForce
関数の引数に、力の量を与えることで、初めて bearing
の角度への力が加わります。
このコードサンプルでは、ドローンは重力に逆らって飛ばなければなりません。クリックしてドローンを飛ばし、それから落下させ、再度ドローンに上向きの力を加えると、落下は徐々に止まり、飛び始めます!
applyForceScaled
関数は、スプライトに加わる力に、質量を掛け合わせます。
この関数を使用して、各スプライトに対し、異なる方向・強さで、重力を与えることができます!
applyForce と applyForceScaled 、両方の関数とも、 x と y の成分を2つの引数として与えるか、もしくはスプライトに bearing
プロパティがあれば、量成分という1つの引数を与えることで設定できます(補足: スプライトに bearing
プロパティがないのに、引数を1つしか与えなかった場合は、(x,
0) が与えられたものとみなされます)。
デフォルトでは、力はスプライトの重心にかかります。ただし、 applyForce
および applyForceScaled
関数は、最後の引数として、x, y プロパティを実装した位置ブジェクトを受け付けます。この位置オブジェクトの x, y
は、スプライトの重心からの相対位置を示すもので、スプライトのどの位置に力がかかるのかは、これにより決定します。
attractTo
関数は、そのスプライトに、対象の位置に引き寄せられる力を加えます。この関数は、x, y プロパティを実装した1つの位置ブジェクトでも、座標を x と y
の2つの数字でも、どちらでも引数として受け付けます。
この例では、電子が原子核の周りを軌道を描いている様子をシミュレーションしています(この視覚的モデルは、現代科学の研究に基づいたものではありません。でも、イカすでしょ?)。
なお、このページで示されている高度な動作関数は、スリーピングスプライト を「目覚めさせる」ことはありません。
Torque (トルク)とは、転がりを引き起こす力のことです。スプライトが自然に転がるようにするには applyTorque
を使います。
この例のように、ロボットの転がりは、上り坂のほうが下り坂よりも遅くなります。
チェインコライダー
チェインコライダーには 3 つの異なるモードがあります: vertex
、 distance
、および line
です。
vertex(頂点) モードを使用するには、 Sprite コンストラクタに頂点の配列を渡します。各頂点の配列は、 [x, y] 座標を含まねばなりません。これらのコードサンプルでは、スプライトの(x, y)位置を、小さな黒い四角で強調表示しています。
コードサンプルのチェインスプライトの頂点を変更して、ボールが床に留まるようにしてみましょう!
distance (距離)モードを使用するには、 Sprite コンストラクタに、1つの (x, y) 座標と、「 distance の配列」の配列を渡します。これらの配列は、前の頂点からの相対 [x, y] の差分の連続です。最初の (x, y) 座標は、チェインの最初の頂点になります。
distance モードは、とても長いチェインを作成するのに最適です。
それでは、このコードサンプルに 5 つの distance を追加して、岩場のデコボコ道のチェインをボールが転がるような地面を作ってみましょう。
line (直線)モードを使用するには、 Sprite コンストラクタに (x, y) 位置と線の長さと角度の配列を渡します。各角度は、直前の線の角度に対する相対値です。
line モードは、小さいチェインや対称的な形のチェインに最適です。
line モードチェインの (x, y) 座標は、全ての頂点座標のちょうど平均の地点であり、必ずしもチェイン上のいずれかの頂点を指すわけではありません。
それでは、コードサンプルの線の長さと角度を変更してみてください!
ポリゴンコライダー
通常のポリゴンは、 Sprite コンストラクタに、一辺の長さと「ポリゴン名」を渡すことで生成できます。
ポリゴン名として使用できる文字列は次の通りです: 'triangle'
(三角形)、 'square'
(四角形)、
'pentagon'
(五角形)、 'hexagon'
(六角形)、 'septagon'
(七角形)、
'octagon'
(八角形)、 'enneagon'
(九角形)、 'decagon'
(十角形)、
'hendecagon'
(十一角形)、 'dodecagon'
(十二角形)。
チェインの始点と終点が同じ座標で、かつ、結果として凸多角形となっていれば、それは自動的にポリゴンとなります!
スプライトが凸多角形のポリゴンなら sprite.shape = 'chain'
で、強制的にチェインに変換できます。
すべてのポリゴン、および閉じたパスのシェイプのチェインは、頂点座標のかたよりにかかわらず、スプライトの中央に配置されます。
これは、5つの頂点を持つ、一般的な星型を作るコードです。
星型は凹多角形であるため、ポリゴンコライダーを持つことはできないことに注意してください。
p5play のホームページにある、ロゴが回転するデモがどのように作られたか、これでおわかりいただけるでしょう!
閉じたチェインの内側は空洞なので、より小さいスプライトをたくさん入れる「うつわ」として使うことができます。
閉じたチェインは、 dynamic コライダーには適していないことに注意してください。
これは p5play が使用している Box2D 物理エンジンの制限です。代わりに「コンボコライダー」ページを参照して、複数の凸多角形コライダーを組み合わせて凹多角形コライダーにする方法を検討してください。
コンボコライダー
addCollider
関数を使用すると、スプライトに複数のコライダーを追加することができます。 Sprite と同じコンストラクタ引数をサポートしていますが、最初の
2 つの引数は、スプライトからの x, y オフセット位置指定となります。
ただしこの機能は、ゲームで本当に必要なときにだけ使用してください! 例えば、迷路の壁のように大量のコライダーを必要とする場合は、独自のコライダーを持つスプライトを大量に生成するだけでいいはずです。また、スプライトの画像が複雑でも、そのコライダーの形状は、プリミティブな長方形や円といった、幾何学的図形で十分なはずです。特に小さなスプライトでは、それが最も適しています。
ならば、実際に複数のコライダーを持つスプライトが必要なのは、どのようなケースでしょう? 例えば、このコードサンプルのような、ピンボールのフリッパーをモデル化したい場合などです!
スプライトにコライダーを追加すると、スプライトの質量が自動的に再計算されることに注意してください。
コンボセンサー
オーバーラップセンサーとは、そのスプライトが別のスプライトとオーバーラップしているかどうかの「検出器」です。
センサーのないスプライトで、オーバーラップ判定のメソッドが使用されると、デフォルトでは暗黙的に addDefaultSensors
関数が実行され、各スプライトのコライダー用にセンサーを生成します。
addSensor
関数を使用すると、スプライトにさらにセンサーを追加することができます。
描画のカスタマイズ
動きのあるスプライトに視覚的エフェクトをかける際、エフェクト用のアニメーションを事前に定義することが困難な場合があります。
幸いなことに、スプライトの draw
関数をカスタマイズすれば、お望みどおりの視覚的エフェクトをかけることができます!
スプライトの draw 関数内部では、スプライトの中心は (0, 0) に移動されることに注意してください。
このコードサンプルでは、スプライトの楕円を移動方向に回転させ、移動速度に比例してその方向に長さを引き伸ばします。ちょっと複雑ですね!
描画更新のカスタマイズ
スプライトの update
関数は、デフォルトでは p5.js
の描画ループの最後に実行されます。この関数は、スプライトのアニメーション(ある場合)と、マウスイベントの状態監視を更新します。
この関数をオーバーライドしても、舞台裏のマジックのおかげで、デフォルトの挙動は置き換えられず、「新しい挙動の追加」となります。
シーケンシャルな移動
このページのコードサンプルで使用している Turtle
スプライトは、昔懐かしい Turtle のプログラミングを思わせる、緑色の三角形です。ただし通常のスプライトと異なり、
Turtle の移動方向は、常にその向き(回転角)と等しくなっています。
async
関数内で await
キーワードを使用すると、ひとつの移動の終了を待ってから、次の移動を開始することができます。これは、スプライトをの移動をあらかじめ決めておいた順番どおり(シーケンシャル)に行うのに便利です。
delay
関数を使うと、指定したミリ秒だけ、次の移動を待つことができます。ちなみに 1000 ミリ秒とは、 1 秒のことです!
move
、 moveTo
、 rotate
、および rotateTo
関数はすべて、移動が終了したときに true へと解決される Promise
を返します。
しかし、そうしたスプライトの移動が、新しい移動指定を受けたり、軌道が大幅に変わるほどのコリジョンが発生したりして妨げられると、その promise は false へと解決されます。
あるスプライトを、移動指定をした別のスプライトに追従させたい場合、そのスプライトが目的地に達するのを待つのでなく、追従させる方のスプライトで moveTo
を何度も繰り返したくなるかもしれません。しかし、パフォーマンスを向上させるためには、そのスプライトと対象の位置との角度を取得する angleTo
関数の使用を検討してください。この角度は、スプライトが移動する方向を変更するために使用できます。
このコードサンプルでは、 p5.js dist 関数が使用されており、プレイヤーとその味方との間の距離を計算しています。