このチュートリアルシリーズでは、スーパーマリオのような横スクロールアクションゲーム、いわゆる「プラットフォーマー」というジャンルのゲームを作っていく。今回は、初回ということで、ゲームのプレイ画面にプレイヤーキャラクターを用意して操作するところまでやってみよう。

Memo:
ゲームを作り始めるのに以下の記事もお役立てください。
Godot をダウンロードする
Godot のプロジェクトマネージャー
Godot の言語設定
Godot で作るブロック崩しシリーズ



新規プロジェクトを作る

右側の「新規プロジェクト」ボタンをクリックする。

新規プロジェクト

プロジェクトを保存したい場所を選択して、右上の「フォルダーを作成」をクリックしてプロジェクトのフォルダ名を入力して「OK」をクリックする。ここではシンプルに「Platformer」と名付けたが、実際に自分のオリジナルのゲームを開発するときは仮でも独自の名前を設定しよう。

フォルダー作成

「現在のフォルダーを選択」をクリックする。

現在のフォルダーを選択

レンダラーには「OpenGL ES3.0」を選択して「作成して編集」をクリックする。Webブラウザで動くようなゲームを開発する場合はレンダラーに「OpenGL ES 2.0」を選択するが、このチュートリアルでは PC ゲームを想定しているので、これで良い。

レンダラーを選択して作成して編集

これでエディターの初期画面が表示された。

エディターの初期画面



アセットファイルを取り込む

まずはプロジェクトに必要なキャラクターや背景の画像ファイル一式(アセット)をまとめて準備しておこう。

ただし、このまま画像のインポートをすると、ドット絵(Pixel Art)の場合、特有のエッジが効いた画像ではなく、ぼやけた感じの画像に補正されてしまう。これを避けるために、事前準備として、インポートのデフォルトの設定を変更する。

具体的には、Godot エディタ左上の「インポート」ドックを開き、「プリセット」ボタンをクリックしたら「2D Pixel」を選択する。

エディターの初期画面

同様に「プリセット」を開き、「‘Texture’のデフォルトとして設定」を選択する。

エディターの初期画面

これでインポートの必要な設定変更ができたので、下記のリンクからアセットをまとめてダウンロードしよう。

Download Assets:
1. Dropboxの共有フォルダ > Assets.zip
2. アセット1: Pixel Adventure
3. アセット2: Pixel Adventure 2

1 は、このチュートリアルのために 2、3 でダウンロードしたファイルを一つにまとめたものです。2、3で直接ダウンロードする場合、制作者の方に寄付したり、作品に高評価をつけたりすることで、結果制作活動を応援することができます。ここで取り扱っているアセットファイルは Creative Commons Zero (CC0) ライセンスの下でリリースされています。ライセンスについて詳しくはこちら をご覧ください。

ダウンロードしたアセットファイルをまとめた「Assets」フォルダを丸ごと、ファイルシステム上の「res://」フォルダ直下にドラッグ&ドロップして取り込む。

ファイルシステムにアセット追加



ゲームのウインドウサイズを設定する

事前準備がもう少しある。実際にゲームをプレイする時のウインドウサイズを設定しておこう。

「プロジェクト」メニュー>「プロジェクト設定」を選択する。

プロジェクト設定

「一般」タブを開いた状態で、「検索」ボタンをクリックして「window」と検索しよう。すると「Display」カテゴリの「Window」という項目が見つかるはずだ。これを選択してウインドウサイズを設定する。
「Size」セクションの「Width(幅)」と「Height(高さ)」を 384 x 256 px に設定する。これから使用するキャラクターのスプライトの画像が 32 x 32 px なので、32 で割り切れるサイズにした。

ウインドウのSizeを設定

続けて「Stretch」セクションの「Mode」を「2d」に変更し、「Aspect(縦横比)」を「keep」にしておこう。

ウインドウのStretchを設定



Level1 シーンを作る

レベルというと、日本ではRPGのキャラクターの成長を表す指標としてよく用いられる単語だが、海外のゲームの場合、日本のアクションゲームやシューティングゲームで階層を表す「ステージ」に相当する単語として用いられる。海外のスタンダードにも慣れておきたいので、このチュートリアルでも「ステージ」ではなく「レベル」という単語を使用していく。

さっそくだが、まずは最初のレベルである「Level1」のシーンを作っていく。
プロジェクトを作ってすぐの画面では、左側のシーンドックでルートノードを生成できる。ここでは「2D シーン」を選択しよう。これを選択すると「Node2D」クラスのノードがルートノードとして作られる。

ルートノードを生成

Node2D ノードの名前を「Level1」に変更しよう。

Node2Dノードをリネーム

そのまま「シーン」メニュー>「シーンを保存」、もしくは Windows: Ctrl + S / macOS: Cmd + S のショートカットでシーンを保存する。このとき、レベルのシーンをまとめるためのフォルダを先々のために用意しておく。まずは表示されたパネルの右上にある「フォルダーを作成」をクリックする。

フォルダーを作成

「Levels」と名付けて「OK」をクリック。

Levelsに変更

Level1 シーンのファイル名を「Level1.tscn」として「保存」をクリックする。

Level1.tscnとして保存

これでゲームの舞台となる「Level1」シーンが用意できた。この舞台に、背景を配置したり、役者であるプレイヤーキャラクターや、敵キャラクター、コインなどのアイテム、ゴールなどを配置して、徐々にゲーム画面を作り上げていく。


プレイヤーキャラクターのノードを作る

「Level1」ノードが同名の「Level1」シーンのルートノードであり、そのルートノードに紐づくノードを子ノードと言う。子ノードにさらにノードがぶら下がって孫ノードとなり、これらのルートの下に紐づく子ノード以下のノードの集まりをブランチという。

シーンを一つの木だとすると、ルートノードが木の幹、ブランチが枝葉である。このような構造を一般的にツリー構造と呼ぶ。Godot の一つの特徴として、このツリー構造を最大限活かして、コンポーネントの構成が非常に分かりやすくなっている。

ではルートノード「Level1」にプレイヤーキャラクターとして必要なノードを追加していこう。「Level1」ノードを選択した状態で、上の「+」ボタン、もしくはショートカット(Windows: Ctrl + A / macOS: Cmd + A)でノードを追加する。
ノードを追加

最初に追加するのは「KinematicBody2D」クラスのノードだ。検索して選択したら「作成」ボタンをクリックすれば、このクラスのノードが追加される。「KinematicBody2D」ノードの名前を「Player」に変更しておこう。
ルートにKinematicBody2Dノードを追加してPlayerに名前を変更

次に「Player」ノードを選択して、子ノードを追加する。「AnimatedSprite」クラスのノードを追加しよう。
PlayerノードにAnimatedSprite追加

同様に、「Player」ノードを選択した状態で、子ノードをもう一つ追加する。今度は「CollisionShape2D」クラスのノードを追加しよう。
PlayerノードにCollisionShape2Dノードを追加

ここまでで、シーンドックはこのように表示されていればOKだ。
シーンドックを確認



AnimatedSprite ノードを編集する

ただ一枚の画像をスプライトとして割り当てるなら「Sprite」クラスのノードを使用するが、スプライトに複数の画像を割り当ててアニメーションさせたいときは、「AnimatedSprite」クラスのノードを使用する。

公式オンラインドキュメント:
AnimatedSprite

シーンドックで「AnimatedSprite」ノードを選択した状態で、右側のインスペクタードックを見てほしい。

では、一番上の「Frames」プロパティを編集する。「空」の箇所をクリックするとプルダウンメニューが表示されるので、「新規 SpriteFrames」を選択しよう。
新規 SpriteFramesを選択

次に「Playing」プロパティをオンにしよう。デフォルトではオフになっているが、それではアニメーションが再生されないから要注意だ。
Playingプロパティをオン

プロパティの編集はひとまずこれだけだ。次に、一番上の「Frames」プロパティの「SpriteFrames」をクリックしよう。
SpriteFramesをクリック

すると Godot エディタ下部の「スプライトフレーム」パネルが開く。ここでアニメーションを作っていく。

スプライトフレームパネルを開く

元々一つ用意されているアニメーションの名前を「defalut」から「idle」に変更しよう。

アニメーションの名前をidleに変更

このアニメーション「idle」にスプライトシート(複数の画像が一つの画像ファイルにまとめられたもの)を割り当てる。「アニメーションフレーム」セクション上部の左から二番目のアイコンをクリックしよう。

スプライトシートの読み込み

このチュートリアルで使うプレイヤーキャラクターは「Pink Man」にしたいので、先ほどインポートしたアセットファイル一式から「res://Assets/Main Characters/Pink Man」フォルダまでたどり、「idle (32x32).png」ファイルを選択して「開く」を押す。

スプライトシートを開く

今度はパネル左上を見てみよう。スプライトシート上で水平方向と垂直方向にいくつずつ画像が分けられているのかを指定する。今回開いたスプライトシートを見ると、画像は横一列に11種類用意されているので、ここでは「水平:11、垂直:1」としておこう。この数値がおかしいと、画像が途中で切れたり、隣の画像が入ってきたりするのでよく見て設定してほしい。

スプライトシートのテクスチャの数を合わせる

続けてパネル右上を見てみると「すべてのフレームを選択/消去」ボタンがある。これをクリックしよう。

全てのフレームを選択

すると、シートの画像が正しい区切りで全て選択できた。

全てのテクスチャが選択された

最後にパネル下部の「11フレームを追加」をクリックする。

11フレームを追加

これでスプライトシート上の全てのスプライトのテクスチャ画像を一括で取り込むことができた。

スプライトシートでテクスチャの取り込み完了

アニメーションの速度を変更しておこう。値は 1 秒あたりのフレーム数(FPS: Frames Per Second)だ。デフォルトは 5 になっており、値が大きくなるほど、アニメーションの動きが速くなる。ここでは 24 とした。

アニメーションの速度を24に設定

ちなみに、アニメーションフレームセクション右上の (+) と (-) ボタンで画像の拡大縮小が可能だ。

アニメーションフレームの拡大と縮小

同様の手順で他のスプライトシートの分もアニメーションを用意しておこう。
なお「Pink Man/Fall (32x32).png」と「Pink Man/Jump (32x32).png」はスプライトシートではなくテクスチャ画像が1つだけのファイルなので、単純にファイルシステムドックからアニメーションフレームへドラッグ&ドロップしたほうが早い。

double_jump

fall

hit

jump

run

wall_jump

インスペクターで「Animation」プロパティの選択を切り替えながら、2D ワークスペース上でそれぞれのアニメーションをチェックしてみよう。

2Dワークスペースでアニメーションをチェック

最終的に作ったアニメーション全てを使うかどうかはまだこの時点ではわからないが、ひとまずプレイヤーキャラクターのアニメーション作成は完了だ。



CollisionShape2D ノードを編集する

ここからは「CollisionShape2D」ノードのプロパティを編集していく。とは言っても、比較的直感的に作業することが多いので、サクッと終えられるに違いない。さっそくコリジョン形状を設定しよう。

公式オンラインドキュメント:
CollisionShape2D

シーンドックで「CollisionShape2D」ノードを選択したら、インスペクターで「Shape」プロパティのプルダウンメニューをクリックし、「新規CapsuleShape2D」を選択しよう。「新規RectangleShape2D」でも良いのだが、スプライトのテクスチャが丸みのあるデザインのため、今回は四角ではなくカプセル型にした。
新規CapsuleShape2Dを選択

次に2Dワークスペースで、今設定したばかりのコリジョン形状を調整していく。まずはドラッグ操作で調整しやすいように、ツールバーのスナッピングオプションアイコンをクリックして、「ピクセルスナップを使用」にチェックを入れておく。これで 1 pixel 単位でコリジョン形状がスナップするので、調整しやすいはずだ。ドット絵の場合は特に便利なので覚えておこう。
ピクセルスナップを有効にする

そのまま2Dワークスペース上で、コリジョン形状の上と右にある 赤い丸 をドラッグしてサイズと形を調整しよう。下のスクリーンショットのように、横幅はスプライトテクスチャの体の幅に、縦の高さはスプライトテクスチャの頭のてっぺんから足までの長さに、それぞれ合わせられたらOKだ。
コリジョン形状の調整

調整が完了したら、「Player」ノードの子ノードである「AnimatedSprite」ノードや「CollisionShape2D」ノードの位置が2Dワークスペース上で誤ってズレてしまわないように、子ノードを選択できないようにしよう。

まずシーンドックで「Player」ノードを選択する。
Playerノード選択

ツールバーの「子ノードを選択不可にする」アイコンをクリックするだけでOKだ。
子ノードを選択不可にする

同じアイコンがシーンドックの「Player」ノードの横に表示された。これで2Dワークスペース上で「Player」の子ノードは選択できない状態になった。
子ノードを選択不可にした



仮の足場を作る

このゲームはプラットフォーマーなので、このあとキャラクターに歩いたりジャンプしたりする動作を設定していく。しかし、その時に足場がないと、キャラクターはゲームが開始するや否や、重力によって画面の下方向に落下して消えてしまう。

そこで、「Level1」ノードに、ひとまず仮の足場として「StaticBody2D」ノードを追加しよう。名前を「TempGround」とでも変更しておくと分かりやすい。さらに「TempGround」ノードに「CollisionShape2D」ノードを追加しよう。ちなみに、マップはあとできちんと作る機会があるので安心してほしい。
Area2Dで仮の足場を追加

「CollisionShape2D」ノードにコリジョン形状を設定する。「Shape」プロパティを「新規RectangleShape2D」とし、「Extents」プロパティ (x, y) を (128, 10) に設定しよう。

2D ワークスペースで「Player」ノードを大体中央に移動し、「TempGround」ノードをその少し下に配置する。これで仮の足場ができた。
PlayerノードとTempGroundノードを移動



インプットマップにアクションを追加する

インプットマップとは、ゲームの入力操作設定だ。キーボードのキーやマウスのクリック、ゲームパッドのボタンなど、ゲームで利用する予定のものを「アクション」として登録することができる。デフォルトの「アクション」もいくつか登録されているが、このチュートリアルではキーボードのキー入力をいくつか追加登録していく。プレイヤーの動きをプログラミングする前に必要な作業だ。

まずは「プロジェクト」メニュー>「プロジェクト設定」を選択する。
プロジェクト設定

「インプットマップ」タブに切り替えたら、追加したい「アクション」(入力操作)に名前をつけて「追加」ボタンで登録する。
インプットマップにアクション追加

以下の4つのアクションを登録しよう。登録できたら「+」ボタンでキーを割り当てていく。

  • move_right
  • move_left
  • jump_force
  • dash
    4つのアクションを追加

登録したアクションの「+」ボタンを押したら、入力装置の種類の選択をする。ここでは「キー」を選択しよう。これはキーボードのキーを意味している。
キーを選択

中央に「確認」のダイアログが出るので、この状態で設定したいキーボードのキーを実際に押下する。例えば、一つ目のアクション「move_right」の場合はキーボードの「右矢印キー」を押下すれば良い。確定するときは「OK」をクリックする。つい「Enter」キーを押下しがちだが、それだと「Enter」キーが割り当てられてしまう。
キーの割り当て

追加した4つのアクションには以下通りにキーを割り当てると良い。もちろん自分好みに変更していただいても結構だ。

  • move_right: Right(右矢印キー)
  • move_left: Left(左矢印キー)
  • jump: Up(上矢印キー)
  • dash: Space(スペースキー)

4つとも登録できたら「閉じる」ボタンをクリックして設定は完了だ。
閉じるで終了



Player ノードにスクリプトをアタッチする

さてここからは、プレイヤーキャラクターの動きを作っていく。動きを作るには、プログラミングが必要だ。プログラミングがまだ不慣れな方は、ひとまずチュートリアルのコードをコピー&ペーストして、プレイヤーキャラクターの動きを見ながら理解を深めていっていただいても良いだろう。

ところで、Godot では GDScriptという独自のプログラミング言語か、別のゲームエンジン Unity でも使用される C# と言うプログラミング言語を使用する。C# の場合、Godot とは別のエディターを使用する必要があったり、文法がやや難しかったりで、経験者向きの側面が強いことから、このチュートリアルでは、文法がより初学者にも理解しやすい GDScript を使用する。GDScript なら、Godot エディター上でそのままコーディング(コードを入力してプログラムを作っていく作業のこと)でき、ドキュメンテーションを Godot エディター上で確認できるので、よりゲーム開発の作業に集中しやすいだろう。

では「Player」ノードにプログラムを記述するためのスクリプトをアタッチしよう。シーンドックで「Player」を選択し、右上の「選択したノードに新規または既存のスクリプトをアタッチする」アイコンをクリックする。
スクリプトをアタッチする

スクリプトの保存先を変更したいので、「パス」の右側にあるフォルダーアイコンをクリックする。
スクリプトの保存先を変更

パネルが開いたら、パスを確認しよう。パスがリソースのルート「res://」になっていなければ、左上の「↑」アイコンをクリックしてルートまで戻り、そこで右上の「フォルダーを作成」ボタンをクリックする。
ルートにフォルダー作成

フォルダーの名前に「Player」と入力して「OK」をクリックして確定する。
Playerフォルダーを作成

パネル上部の「パス」が今作成したフォルダーのパスになっていればOKだ。ファイル名を「Player.gd」として「開く」をクリックしよう。ちなみに、ファイルの拡張子 .gd は GDScript ファイルのことを指している。
Player.gdスクリプトのパスを決定して開く

パスが変更されたのを確認したら「作成」ボタンをクリックする。
作成をクリック

Godot エディタの中央が、2D ワークスペースからスクリプトエディタに切り替わり、今アタッチしたばかりのスクリプト「Player.gd」が開いた状態になった。これから、このスクリプトを編集していく。
スクリプトエディタの表示


プレイヤーキャラクターの動きを実装する

まず、デフォルトで入力されているコメントや_readyメソッドはこのスクリプトでは不要なので、削除しておこう。
デフォルトのボイラープレートを削除

次に必要なプロパティ(クラス内で定義する変数のこと)を定義していく。下記のコードを「Player.gd」スクリプトの 3 行目以下に記述してほしい。

export var acceleration = 256
export var max_speed = 64
export var max_dash_speed = 80 
export var friction = 0.1
export var gravity = 512
export var jump_force = 224
export var air_resistance = 0.02
var velocity = Vector2.ZERO

コードの先頭にexportがついているプロパティは、その値をインスペクターでも編集できるようになる。試しに「player」ノードを選択してインスペクターを見てみよう。スクリプトに記述したプロパティが表示されているのがわかるだろう。スクリプトで定義した値がデフォルトの値になっている。
exportのプロパティをインスペクターで確認

exportキーワードをつければ、スクリプト上でプロパティの値を直接編集しなくても、インスペクター上で気軽に編集できる。デバッグしながら値の微調整が必要になると思われるプロパティにはexportをつけるのがおすすめだ。

では今回定義したプロパティについてそれぞれ説明しておこう。

  • acceleration: プレイヤーキャラクターの左右移動操作時の加速度。
  • max_speed: プレイヤーキャラクターの左右移動操作時の最大速度。
  • max_dash_speed: プレイヤーキャラクターの左右ダッシュ移動操作時の最大速度。
  • friction: プレイヤーキャラクターの左右移動操作をやめた時に受ける摩擦抵抗。
  • gravity: プレイヤーキャラクターが常に画面下方向に受ける重力。
  • jump_force: プレイヤーキャラクターのジャンプ操作時のジャンプ力。
  • air_resistance: プレイヤーキャラクターのジャンプ操作時に受ける空気抵抗。
  • velocity: 方向の要素を持ったプレイヤーキャラクターの速度、デフォルト値はVector2型で(0, 0)。値はこのあとのプログラミングによりリアルタイムで更新される予定。

さらにスクリプトの一番下の行で、spriteプロパティをonreadyキーワードをつけて定義しよう。

onready var sprite = $AnimatedSprite

spriteプロパティの値は、「Player」ノードの子ノードである「AnimatedSprite」ノードを定義した。onreadyをつけたプロパティは、全てのノードの読み込みが終わってから定義される。ノードの読み込みは親ノードの方が子ノードより先なので、子ノードをプロパティに格納したい場合は、onreadyキーワードを利用してプロパティを定義する。

このようにして、子ノードのプロパティやメソッドにアクセスする予定がある場合は、プロパティとして定義しておくと、このあとのコーディングが楽になる。また、シーンツリー(シーン内のノードの構成)に変更があっても、このプロパティの値だけ変更すれば良いので、メンテナンスがしやすいというメリットもある。Godot でのゲーム開発では割と一般的な手法だ。

次に定義するのは_physics_processメソッドだ。このメソッドは「Node」クラスの組み込み関数だ。「KinematicBody2D」クラスは「Node」クラスを継承しているので利用できる。

_physics_processメソッドは、物理フレーム(デフォルトでは1秒間に60フレーム)ごとにメソッド内のコードを実行してくれる。これを利用して、プレイヤーの入力操作によって、プレイヤーキャラクターの動きを制御することができる。

では具体的にやっていこう。スクリプトの一番下に次のコードを記述しよう。

func _physics_process(delta):
	velocity.y += gravity * delta

_physics_processの引数deltaは1フレームあたりの秒数(デフォルトでは1/60秒)だ。velocity.yの値にgravitydeltaを乗じた値を足している。ゲーム開発では一般的に、2次元の x, y 座標で y 軸の値は、画面の下にいくほど大きく、上にいくほど小さくなる。毎フレームgravityの値をvelocityy に加算することで、プレイヤーキャラクターを常に画面下方向に動かす力を加えている。これにより、キャラクターに重力がかかっている状態を作ることができた。

さらに、次のコードを_physics_processメソッドに追加しよう。

	var x_input = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	
	if x_input != 0:
		velocity.x += x_input * acceleration
		velocity.x = clamp(velocity.x, -max_speed, max_speed)

メソッド内でx_input変数を定義した。Inputクラスのget_action_strengthメソッドは、引数のインプットアクションが入力されれば 1 を、されなければ 0 を返す。つまり、プレイヤーが右矢印キーを押せば 1 、左矢印キーを押せば -1 、どちらも押さなければ 0 の値がx_inputに入る。

その次のif x_input != 0:の if 構文は、右か左の矢印キーいずれかを押した場合、ということになる。

velocity.xに、x_inputaccelerationを乗じた値を加算している。これで、プレイヤーが右か左の矢印キーを押した方向へ、毎フレーム加速しながらプレイヤーキャラクターを移動させる仕組みができた。

しかし、このままだと毎フレーム加速し続け、とんでもないスピードでキャラクターが移動してしまう。そこで、組み込みのclampメソッドを利用して、velocity.xの値が最大値および最小値の範囲内に収まるようにする。clampメソッドは最大値に第二引数、最小値に第三引数、どちらにも達していなければ第一引数を返す。

さらに、プレイヤーキャラクターにダッシュ機能を実装したいので、インプットアクション「dash」の入力操作、つまりスペースキーを入力しているかどうかで、条件分岐させた。if x_input != 0:のブロックは以下のように変更した。

	if x_input != 0:
		velocity.x += x_input * acceleration
		if Input.is_action_pressed("dash"):
			velocity.x = clamp(velocity.x, -max_dash_speed, max_dash_speed)
		else:
			velocity.x = clamp(velocity.x, -max_speed, max_speed)

さらにif x_input != 0:ブロックの最後にこのコードも追加しよう。

		sprite.flip_h = x_input < 0

スプライトのテクスチャの水平方向に反転させるプロパティflip_hは、x_inputが 0 より小さい値(つまり左矢印キーを押下して -1 )になっているときに有効になるようにした。

velocityの値を実際のキャラクターの移動に反映させるために、「KinematicBody2D」クラスの組み込みメソッドであるmove_and_slide_physics_processメソッドの最後に追加する。

	velocity = move_and_slide(velocity, Vector2.UP)

このmove_and_slideメソッドは、第一引数に割り当てた速度に delta を自動的に乗じて、このスクリプトがアタッチされている「KinematicBody2D」クラスのノードを移動してくれる。第二引数は、上方向がどちらかを指定する。プラットフォーマーは画面の上方向がそれに当たるのでVector2.UPを割り当てた。これによって、自動的に天井、地面、壁がどの方向なのかを判別してくれる。

では一旦ここでプロジェクトを実行してキャラクターの動きを確認してみよう。

デバッグパネルで、足場である「TempGround」ノードのコリジョン形状が見えるようにするため、先に「デバッグ」メニュー>「コリジョン形状を表示」にチェックを入れておこう。
デバッグ>コリジョン形状を表示にチェック

Godot エディタ右上の「プロジェクトを実行」アイコンをクリックする。
プロジェクトを実行

まだメインシーンを選択していない場合はダイアログが表示されるので、「現在のものを選択」をクリックする。
現在のものを選択

デバッグパネルが開くので動きを確認してみよう。
動作確認

重力は機能して、キャラクターはきちんと足場に落ちている。スプライトのテクスチャもflip_hプロパティの切り替えにより向きが変われば反転している。しかし、左右の移動は右または左の矢印キーを一回押下しただけで滑り続けて止まらない状態だ。さらに必要なコードを追加していこう。


今度は、velocity = move_and_slide(velocity, Vector2.UP)のコードより前に、以下の if ブロックのコードを追加しよう。

	if is_on_floor():
		if x_input == 0:
			sprite.play("idle")
			velocity.x = lerp(velocity.x, 0, friction)
		else:
			sprite.play("run")

is_on_floorメソッドは、現在地面にキャラクターが接しているかを判定してくれる。接していれば戻り値が true になる。if x_input == 0:は左右移動の入力がない場合という条件の if 構文だ。この if / else ブロック内を見ていく。

まず、sprite(「AnimatedSprite」ノード)のplayメソッドによりアニメーション「idle」を再生する。

次に、velocity.xの値は組み込みのlerpメソッドにより、現在のvelocity.xの値の間を第三引数であるfrictionプロパティの重み分だけ、第二引数 0 に近づけていく。_physics_processメソッドが毎フレーム実行されることにより、次第に値は 0 になる。

else ブロックの方は、左右移動の入力があった場合という条件になり、このとき「AnimatedSprite」ノードのplayメソッドによりアニメーション「run」が再生される。

それでは、改めてプロジェクトを実行して、滑らなくなったか、アニメーションが切り替わるか確認しよう。
プロジェクトを実行して滑らなくなったか確認

lerpメソッドによる摩擦抵抗の実装で、左右移動の入力がなければ減速して停止するようになった。スペースキーを押下した時のダッシュの挙動も上々だ。ちなみに、何度かmax_dash_speedの値を変更して検証してみたが、80 ではあまり移動速度が変わらなかったので 200 に変更した。せっかくなので、あなたもインスペクターからお好みの速度に調整してみよう。


次はキャラクターがジャンプできるようにしょう。続けてif is_on_floor():ブロック内に次のコードを追加しよう。

		if Input.is_action_just_pressed("jump"):
			sprite.play("jump")
			velocity.y = -jump_force

この if ブロックif Input.is_action_just_pressed("jump"):は、(地面にキャラクターが接している時に)インプットアクション「jump」の操作、つまり上矢印キーを一回押した場合、という意味合いだ。この場合「AnimatedSprite」ノードのplayメソッドによりアニメーション「jump」を再生する。そして、この瞬間にvelocity.yにジャンプ力を示すjump_forceを引き算している。引き算するのは、先ほどお伝えした通り、画面上方向にいくほど y 軸の値が小さくなるからだ。

ではプロジェクトを実行して、ジャンプ操作も確認してみよう。

プロジェクトを実行してジャンプの動作確認

今のところ大きな問題はなさそうなので良しとしよう。

あとは少し細かいが、ジャンプ中に左右移動の入力がなかった場合と、ジャンプしてすぐに上矢印キーを離した場合、このそれぞれの動きをコーディングしていこう。

if is_on_floor():ブロックに続けて、以下のelseブロックを追加して、if / else構文にする。

	else:
		if x_input == 0:
			velocity.x = lerp(velocity.x, 0, air_resistance)
			
		if Input.is_action_just_released("jump") and velocity.y < -jump_force / 2:
			velocity.y = -jump_force / 2

elseブロックは、is_on_floorメソッドの戻り値が false だったら、と言い換えられる。つまり、「地面に接していなかったら(空中だったら)」という意味になる。さらにネストされたif構文が続く。一つ目のifブロックの条件は、if x_input == 0:で、これは「左右移動の入力がなかったら」という意味だ。その場合、velocity.xlerpメソッドにより、air_resistanceの空気抵抗により毎フレーム 0 に近づいていく。

elseブロックにネストされて2つ目のif構文を見てみよう。条件式は2つの条件がandで結合されていて、「2つとも満たした場合」という条件になる。

一つ目の条件は、Inputクラスのis_action_just_releasedメソッドを利用した、(空中で)一度「jump」アクション操作(上矢印キー)を離した場合、という内容だ。

2つ目の条件は、velocity.yの値が-jump_force / 2(ジャンプ力の2分の1)未満の場合、という内容だ。

これらを合わせると、空中でスペースキーを離した時にプレイヤーキャラクターの y 軸方向の速度がジャンプ力の半分未満だったら、という意味になる。これを満たした場合、velocity.yの値には-jump_force / 2(ジャンプ力の半分)の値を適用する。これにより、スペースキーをちょんと押しただけだと、低いジャンプになる。

それでは、プロジェクトを実行して変更した内容を確認してみよう。
ジャンプの高さと空気抵抗の影響を確認

スペースキーを押し続けた時と、ちょんと押した時とで、ジャンプする高さが変わった。また、ジャンプ中に左右移動の入力をしない場合、空気抵抗の影響を受け、x 方向の移動距離が少し落ちた。

ひとまずプレイヤーキャラクターの動きが想定通りになったので、「Player.gd」スクリプトの編集は一旦ここまでにしておこう。



Part 1 で編集したスクリプトのコード

今回編集した「Player.gd」スクリプト全体のコードを最後に共有しておこう。何か動きが思い通りに行かない場合はこちらを参考にしてほしい。

Player.gd スクリプトを確認する
extends KinematicBody2D

export var acceleration = 256
export var max_speed = 64
export var max_dash_speed = 80
export var friction = 0.1
export var gravity = 512
export var jump_force = 224
export var air_resistance = 0.02
var velocity = Vector2()

onready var sprite = $AnimatedSprite


func _physics_process(delta):
	velocity.y += gravity * delta
	var x_input = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	
	if x_input != 0:
		velocity.x += x_input * acceleration
		if Input.is_action_pressed("dash"):
			velocity.x = clamp(velocity.x, -max_dash_speed, max_dash_speed)
		else:
			velocity.x = clamp(velocity.x, -max_speed, max_speed)
		sprite.flip_h = x_input < 0
	
	if is_on_floor():
		if x_input == 0:
			sprite.play("idle")
			velocity.x = lerp(velocity.x, 0, friction)
		else:
			sprite.play("run")

		if Input.is_action_just_pressed("jump"):
			sprite.play("jump")
			velocity.y = -jump_force

	else:
		if x_input == 0:
			velocity.x = lerp(velocity.x, 0, air_resistance)

		if Input.is_action_just_released("jump") and velocity.y < -jump_force / 2:
			velocity.y = -jump_force / 2

	velocity = move_and_slide(velocity, Vector2.UP)


Player ノードをシーンにする

最後に「Player」ノードを個別のシーンとして保存し、「Level1」シーンには「Player」シーンのインスタンスをノードとして追加させるようにする。今後、別の新たなブランチをルートノードに追加する場合も、「Player」ノードと同様に、先に別シーンとして作成してから、そのインスタンスをルートノードに追加していくことになるだろう。

実は Godot では、このように個々のシーンのインスタンスを別のより大きなシーンの子ノードとして追加するのが一般的だ。ゲームの小さい部品を作って、それらを集めて少し大きな部品のシーンを作る、ということを繰り返して、少しずつゲームの規模を大きくしていくというわけだ。とはいえ、Godot でのゲーム開発をはじめ、オブジェクト指向プログラミング全般がそういうものだとも言えるだろう。大きく複雑なプログラムも小さくて簡単なプログラムの組み合わせに過ぎない、と考えれば、ゲーム開発はまったくもって怖くない。

では具体的な作業を進めよう。

シーンドックで「Player」ノードを右クリックして「ブランチをシーンとして保存」を選択する。
Playerノード以下のブランチをシーンとして保存

シーンの保存先パスを「res://Player」とし、ファイル名を「Player.tscn」として、「保存」をクリックする。
Player.tscnを保存

すると「Level1」シーンの「Player」ノードの右側に「エディターで開く」アイコンが表示される。これは別のシーンのインスタンスであることを示している。このアイコンをクリックしてみよう。
Playerインスタンスノード右側にエディターで開くアイコン

するとシーンドックと2Dワークスペースが「Player.tscn」シーンの表示に切り替わる。これで「Player」ノードをシーンにすることができた。
Playerシーンに切り替わる

最後に、後処理を少しやっておこう。まず「Player.tscn」シーンのまま、インスペクターの「Position」プロパティを見て見よう。今までのまま(192, 80)のように、(0, 0)以外の値になっているはずだ。これを、「Player.tscn」シーン上では (0, 0)にしておこう。なぜならこのシーン上でプレイヤーキャラクターの位置を変更する必要がないからだ。
Player.tscnでpositionを変更

続けて、2Dワークスペースにて「Level1」タブをクリックして「Level1.tscn」シーンに戻そう。
Level1シーン上でPlayerノードの場所調整

同じくインスペクターから「Position」プロパティを (0, 0) から (192, 80)などに変更しておこう。これでさっきデバッグしていた時と同じ状態に戻った。
Playerノードのpositionを開く


おわりに

以上で Part 1 は完了だ。プレイヤーキャラクターが自分の操作で想定通りに動くと嬉しいものだ。ゲーム開発のモチベーションを高めるには、最初にプレイヤーキャラクターを作るというのが実践しやすい一つの方法ではないだろうか。

次回はタイルマップを作ってゲームの世界を作っていくのでお楽しみに。



UPDATE
2022-10-16 仮の足場を作るの手順の「Area2D」の誤記を「StaticBody2D」に訂正
2022-03-26 Dropbox のアセットの内容に Part 12 で利用するファイルを追加してリンクを更新
2022-03-24 Dropbox のアセットの内容に Part 11 で利用するファイルを追加してリンクを更新