Godot スプライトのアニメーションと移動
2024年11月18日
はじめに
Godot4.xの2Dゲーム開発の基礎として、
- スプライト(イラスト)の表示
- キー入力による移動
- アニメーションの設定
これらを、ご説明します。
最終的には下記のようなものになります。
スプライト(イラスト)の表示
シーンを作って、まずは簡単にスプライトを表示します。
絵素材がないよ、という方は、下記の画像を使ってください。
まずは、アニメーションのない簡単な表示をやってみましょう。
新規プロジェクトを作り、2Dシーンを作成しましょう。さっそく、シーンを保存します。"Ctrl+S"で、保存できます。
保存すると、名前と保存場所を決めるウィンドウが表示されるので、名前をmain_game.tscn
にして保存します。
シーンパネルの、Node2Dを右クリックして"子ノードの追加..."を押します。
どんなノードを作りたいのか、リストアップされます。ここでは、Sprite2Dを選択しましょう。2D空間にイラストを表示できるノードです。検索にSpriteと入力すると、探しやすいです。Sprite2Dを選択して"作成"のボタンを押します。
次に、作ったSprite2Dにテクスチャ(画像)を設定します。
ファイルシステムに、お好みの画像をドラッグ&ドロップで放り込んでください。ない場合は、上で提示したa_05_002.png
を使ってください。
Sprite2Dのノードを選択していると、インスペクタにその設定が表示されます。そのSprite2Dの項目の一番上にTextureというのがあり、<空>になっています。ここに、先ほどファイルシステムへ放り込んだ画像を、そこからドラッグ&ドロップで設定します。
これで設定できました。ただ、画像が小さいため、拡大しましょう。
インスペクタのNode2Dの下にTransformがあるので、これを開くとScaleが調整できます。x、yともに10、つまり10倍にします。
しかし、残念なことに絵がぼやけてしまいます。
これは、拡大時に補間処理をしているためです。大きい画像だと都合がいいのですが、ドット絵には適しません。そこで、
プロジェクト > プロジェクトの設定... > 一般 > レンダリング > テクスチャ を選択して、キャンバステクスチャの"デフォルトのテクスチャフィルタ"を"Nearest"にします。
これで補間されなくなります。
2Dビューで、確認すると左上にあるので中央に移動させましょう。2Dビューでドラッグして真ん中付近に移動させてもいいですし、インスペクターのTransformで値を設定してもOKです。
キー入力による移動
キー入力による移動を行いましょう。まず簡単に、Aキーで左、Dキーで右に移動するようにします。
やることは、キー入力を取得できるように設定すること。Sprite2Dにスクリプトを設定し、移動の処理を書くことです。
まず、したごしらえとして、Sprite2Dのノードの名前を"Player"に変更しましょう。
シーンパネルでノードを右クリックして"名前の変更"か、ノードを選択してF2キーで編集できます。
インプットマップの設定
つづいて、プロジェクト > プロジェクト設定 > インプットマップ を開きます。
ここで、キーボードやゲームパッドなどの入力に名前を付けて、どんな入力があったかを扱えるようになります。
"新しいアクションの追加"と書かれているバーに、move_left
と入力し、"+追加"のボタンを押しましょう。
するとリストに、move_left
という項目が追加されました。この項目の右端に"+"ボタンがあるので押して、イベントを追加します。
ウィンドウが出てくるので、Aキーを入力して、"OK"ボタンを押します。
これは、右に移動したときのイベントを作っていくぞ、とmove_left
という受け皿を作りました。その受け皿に、では、どんな入力に反応するか、Aキーで反応するよ、と設定したわけです。
複数のキーも設定できるので、Leftキーなどを設定することもできます。また、キーボードではなく、もしゲームパッドを持っていたら、そちらの右ボタンをここで設定することもできます。
同じように、move_right
という項目を作って、Dキーを設定しましょう。
スクリプトを張り付けてまずは実行してみよう
設定できたらプロジェクトウィンドウは閉じ、次はスクリプトを作っていきます。
シーンパネルからPlayerを選択し、インスペクタのScriptの<空>をクリック、"新規スクリプト..."をさらにクリックします。
すると、スクリプトの作成ウィンドウが開きます。名前はノードの名前がつけられています。Player.gd
となっていたらOKですので"作成"ボタンを押します。
すると下記のようなコードが表示されます。
extends Sprite2D
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
extends Sprite2D
はSprite2Dノード用のスクリプトであることを示しています。ここを示しておくと、Sprite2Dにまつわる情報や処理の呼び出しができます。
関数は二つあります。今回関わるのは、func _process(delta: float)
の関数です。ここは、ゲームを実行すると、不定期で繰り返し呼び出される関数で、ここで、ノードの状態を更新します。
ひとまず、下記のように書き換えます。
extends Sprite2D
# 移動速度(ピクセル/秒)
@export var speed: float = 200.0
func _process(delta: float) -> void:
# 移動方向の計算
var direction = 0
if Input.is_action_pressed("move_left"):
direction -= 1
if Input.is_action_pressed("move_right"):
direction += 1
# スプライトの位置を更新
position.x += direction * speed * delta
そして、F5で、実行しましょう。最初に実行しようとすると、ゲーム起動時に動かすシーンの設定が求められるので、"現在のものを選択"を押します。
しばらくすると、ゲームウィンドウが起動します。Aキー、Dキーで操作できるでしょう。
変数、メンバ変数、インスペクタで編集できる変数
さて、スクリプトの内容を説明していきましょう。
@export var speed: float = 200.0
は、speed
という変数に移動速度を設定しています。
@export
としておくと、インスペクタ上で編集できる数値になります。なるべく、インスペクタで編集してよい値は、このようにしておくといろいろなメリットがあります。データと処理を分離しておくと、変更していい場所が明瞭になり、後々の編集で不具合を起こしてしまう可能性を減らすこともできます。
@export
をつけずに、var speed: float = 200.0
でも動作します。インスペクタ上で編集できませんが、ノードごとに紐づいたメンバ変数として、関数から呼び出して使うことができます。
float
という単語が出てきていますが、これは変数は浮動小数をあつかうよ、という宣言です。型の指定とも言います。
代表的な型は下記の三つです。
-
int
: 0、1、55、-9999、のような整数をあつかいます -
float
: 0.1や、300.0、のように少数をあつかえます -
String
: "おはよう"、のように文字列をあつかえます
intとfloatは同じ数値をあつかえますが、floatは誤差が生まれることがあるので、intでよいものはintにしておく、くらいの気持ちで最初は使い分けるといいです。
例えば、スコアやヒットポイントなど、少数を使う必要がないものは整数のintです。
関数、引数、インデントによる構造化
func _process(delta: float) -> void:
の関数について説明しましょう。
func
は、関数をこれから定義していきますよ、という意思表示です。その後に続く_process
が関数の名前です。
その後の、(delta: float)
は、delta
というfloat型の変数に何か情報が入ってくるので、関数内で利用できるよ、という引数が明記されています。
deltaは、前回更新してからどれくらいの時間が経過したか、秒単位で教えてくれます。
_processは、一定時間ごとに呼ばれるわけではなく不定期ですので、移動の処理を書くときはdeltaに応じて、移動量を変えないと、パソコンの調子がいいときは、スラスラ移動するのに、スペックの低いパソコンだとゆっくり移動してしまう、と、おかしい状態になってしまいます。
-> void
は、この関数は何も値を返さないよ、ということを示しています。例えば-> int
だったら、整数を返しますよ、という定義になります。
最後に:
がくっついていて、基本的には、その後、この関数内の処理はインデントと呼ばれるスペースやタブで行の先頭を一定にあけて、ひとまとまりと認識できるようにします。GDScriptの場合は、これは視覚的な意味合いではなく、そのようなコードであると解析、実行されます。
例えば、関数が二つあった場合は下記のようにして、abc関数とefg関数の中身がどこまでなのか、を明示しています。
func abc() -> void:
var a = 0
a = a + 1
func efg() -> void:
var b = 10
b = b * 2
GDScriptやPythonは、こうした行の頭の空白を利用してプログラムの構造、まとまりを指定します。
違うC#という言語は下記のようになります。
void abc() {
var a = 0;
a = a + 1;
}
void efg() {
var b = 10;
b = b * 2;
}
C#は{}
を利用して、構造を作っています。見た目が分かりやすいように、同じようにインデントをしていますが、処理上の意味はありません。C++、JavaScriptなど、こちらの方式をとっている言語はたくさんあります。
関数の処理について
話はもどって、関数の中身について説明しましょう。具体的には下記のことをしています
- directionの変数を作って0で初期化
- Aキーが押されていたら、directionの値を-1に
- Dキーが押されていたら、directionの値を 1に
- スプライトの位置を更新
まとめると上記の4つです。少し、directionを使った計算がなされているので、理解のために、それを省いて2つの段階で書き直してみましょう。
- Aキーが押されていたら、左に移動
- Dキーが押されていたら、右に移動
この考えで、記述すると下記のようになります。
func _process(delta: float) -> void:
if Input.is_action_pressed("move_left"):
position.x -= speed * delta
if Input.is_action_pressed("move_right"):
position.x += speed * delta
説明を三つの段階に分けましょう。
- if文 条件分岐
- 入力に応じてtrueが受け取れる関数
- 座標の更新
まず、if文です。指定された条件が正しい時、その区間の処理をします。日本語で表現するなら下記のようにできます。
もし、雨が降っていたら:
傘を持って出かける
フォーマットを示すと
if [条件A]:
条件Aが成立しているときの処理
と言いう書き方をします。処理は複数行書くことができます。ここでも行頭の空白、インデントを使って構造化しています。
今回であれば、"move_left"が押されているかどうか、ということになります。
if文の細かい説明はGDScriptについてまとめた時に細かく説明したいと思います。
つづいて、if文の条件の真偽を獲得する方法が必要です。今回であれば、キーが押されているかどうか。それが、Input.is_action_pressed("キーワード")
です。インプットマップで設定したキーワードのものが、押下されているならtrue(真)、いないならfalse(偽)を返してくれるのがこの関数です。
最後に、座標の更新です。position.x -= speed * delta
となっています。Sprite2DのノードはNode2Dの情報を持っているので、2次元の座標をおさめたpositionという変数を持っています。そこにアクセスしているわけですね。
-=
という書き方は、右側の計算結果の値だけ、減らしてください、という意味です。少し具体例を出しましょう。
func abc() -> void:
var a = 0
a += 1 # aは1になる
a = 5 # aは5になる
a += 2 # aは7になる
a -= 9 # aは-2になる
というふうに、aの値は=、+=、-=、などで変えることができます。
if Input.is_action_pressed("move_left"):
position.x -= speed * delta
ここではmove_leftのため、左に移動させたいので、移動量だけ値をマイナスさせたいのです。
speed * delta
は、1秒間でspeedで指定したピクセル分だけ動くように調整しています。
speedは今だと200.0です。1秒おいての更新なら、deltaは1になり、speed * delta
は200なので、左に200ピクセル移動します。
これが、deltaの値が0.1、つまり0.1秒だったら、200×0.1=20となり、左に20ピクセルの移動となります。
このように、_processの関数は不定期でdeltaの値が変動することを想定して、更新処理を作る必要があります。
move_right、右への移動も同様です。
さて、それでは、最初に複雑に書いていたあれは何だったのか、コードをもう一度、提示して説明しましょう。
func _process(delta: float) -> void:
# 移動方向の計算
var direction = 0
if Input.is_action_pressed("move_left"):
direction -= 1
if Input.is_action_pressed("move_right"):
direction += 1
# スプライトの位置を更新
position.x += direction * speed * delta
わざわざdirectionという変数をいったん作って、最後に、まとめて位置を更新する、ということをやっています。
これはよくある処理のまとめ方で、状況の把握と適応を分けているのです。
- キー入力の状況を把握し、情報をまとめる
- まとまった情報をもとに状態を更新する
というふうにしています。このような構造にしておくと、今は攻撃を受けた直後で移動速度が落ちているからその補正をかけよう、今は無敵モードでスピードが二倍だ、というときの補正をかけやすくなります。構造化されているので、スクリプトを編集するときに、キー入力について編集しよう、とか、状態の更新について編集しよう、と意識を集中させやすくなります。
アニメーションの設定
せっかく移動できたので、簡単なアニメーションを実装してみましょう。
Playerとは別に、大元のノードから子ノードを作成します。作成するノードはAnimatedSprite2Dです。
このノードはPlayerBという名前にしましょう。
まず、PlayerBのインスペクターのAnimationのSprite Frameが<空>なのをクリックし、"新規SpriteFrames"をクリックします。
これで、スプライトアニメーションが設定できます。SpriteFrameを持つノードを選択していると中央下に、スプライトフレームが表示されます。ここに、スプライトアニメーションを設定していきます。
ここから、3つの状態を作り、それぞれ絵を設定していきます。
- default : キー入力しない状態
- move_left : 左に移動中
- move_right : 右に移動中
defaultは存在しているので、まずはここに絵を設定しましょう。
まず、ファイルシステムに、絵素材をドラッグ&ドロップします。もしくは、先ほどPlayerで使った画像をつかってもいいでしょう。
つづいて、アニメーションでdefaultを選択して、スプライトを追加します。マス目のアイコンをクリックします。すると、ファイルが選択できるので、追加した絵素材を選択します。
フレーム選択画面になります。Playerで使った画像は区切る必要はないので、水平を1、垂直を1にして、画像をクリックし、"1フレームを追加"をクリックします。
すると、アニメーションフレームに画像が追加されます。
続いて、move_leftを追加しましょう。アニメーションの左上のアイコンで、アニメーションを追加します。new_animationという名前のアニメーションが作られるので、それを一回クリックし、すこしまってもう一回クリックすると名前を変えることができます。move_leftに変更します。
そして、アニメーションさせるためにファイルシステムに画像を追加しましょう。下記の画像を使ってください。
この画像は4つの画像が縦横2x2でくっつけられた画像です。上の二つが、左キー入力用、下の二つが、右キー入力用です。
つづいて、先ほどと同じように、フレームを追加しますが、今回は追加した画像を使います。水平は2、垂直を2にして、上段のフレームを二つ選択して、"2フレームを追加"をクリックします。
さらに、move_rightをつくり、下段のフレームを二つ選択して、"2フレームを追加"をクリックします。
配置されたフレームは、移動させたり、コピーしたり、削除もできますが、今回は必要ありません。
これで、各状態のアニメーションがつくれました。
さて、2Dビューにもどって、インスペクターのNode2DのScaleをxとyを10にして、画面の適当な場所に移動させましょう。
スクリプトの設定
では、スクリプトを設定しましょう。名前はplayer_b.gd
としましょう。
内容は下記のとおりです。
extends AnimatedSprite2D
func _ready() -> void:
# アニメーションを再生する
play()
func _process(delta: float) -> void:
# アニメーションのキーワードを入力に応じて決定する
var animetion_keyword = "default"
if Input.is_action_pressed("move_left"):
animetion_keyword = "move_left"
if Input.is_action_pressed("move_right"):
animetion_keyword = "move_right"
# アニメーションを設定する
set_animation(animetion_keyword)
まず_ready関数は、ノードの初期化用の関数で、ノードが生成されるときに一度呼び出されます。
ここで、play()関数を呼んでおくことで、アニメーションが再生されるようになります。
_process関数の内容は、前述したものと構造は同じです。
- 入力に応じてアニメーションをどうするか情報をまとめる
- アニメーションを設定する
ですね。
set_animation("キーワード")
で、キーワードは、アニメーションで作った名前を指定します。
こうすることで、アニメーションさせることができます。
さいごに
さて、イラストの表示、移動、アニメーションとやってきました。移動とアニメーションは別になっていますので、移動もアニメーションもやるにはどうしたらいいか、挑戦してみるのも面白いと思います。
移動も、左右しかありませんから、上下も加えるならどうしたらいいか、今回の延長で考えていくことができます。
今回は、いろいろ動かすことができたので、次回は、配布できる形への出力について説明します。まず大切なのは、一通りやってみることです。