English version is available here:
Godot: Implementing Pixel-Perfect Movement
XeGrader plus では、レトロな雰囲気を出すために、その動きは全て正確なドット単位に補正しています。通常、座標の用いるのは Godot の Vector2 という浮動小数ベクトルです。例え、背景に 320x200 ドットの画像を用いたとしても、クライアントエリアが 1440x900 ドットになっていれば、その実座標単位で描画位置が処理されるため、正確なドット単位の移動にはなりません。今回は、Godot で正確な 1ドット単位の移動を行う実装について解説していきます。
Godot: Implementing Pixel-Perfect Movement
XeGrader plus では、レトロな雰囲気を出すために、その動きは全て正確なドット単位に補正しています。通常、座標の用いるのは Godot の Vector2 という浮動小数ベクトルです。例え、背景に 320x200 ドットの画像を用いたとしても、クライアントエリアが 1440x900 ドットになっていれば、その実座標単位で描画位置が処理されるため、正確なドット単位の移動にはなりません。今回は、Godot で正確な 1ドット単位の移動を行う実装について解説していきます。
※ Pixel Snap
Godot では、1ドット単位の表示をサポートする Pixel Snap という機能があります。この Pixel Snap は、実行環境のモニター解像度に合わせてスナップしてしまうため、ゲーム内の解像度では正確な 1ドットを維持できません。
- ノード構成
おそらく一般的には、ゲームに登場させるキャラクターは以下のようなノード構造になると思います。
+ Node2D(キャラクタールート, スクリプトの位置) ├ CharacterBody2D (物理演算用) ├ CollisionShape2D (当たり判定) └ Sprite2D (表示用)
そして、CharacterBody2D の position または global_position を動かすことで、キャラクターを移動させると思います。velocity に方向と速度を設定して、move_and_slide() で実際に動かす。これが標準的な使い方です。ですが、これでは正確なドット単位の実装は不可能です。理由はシステムが計算する座標の上に表示が載っているためです。そこでノード構成は次のように変更します。
+ Node2D(キャラクタールート, スクリプトの位置) ├ CharacterBody2D (物理演算用) │ └ CollisionShape2D (当たり判定) └ Sprite2D (表示用)
移動処理と表示を完全に分離します。これで移動結果に表示が影響されなくなります。
- ドット単位に表示を補正する
さて、実態と表示を切り離したため、このままでは実態に表示が追いつきません。そこで自分で Sprite2D に座標を与えることになります。ここで小数点以下を四捨五入してからSprite2D に座標を与えることで、正しいドット単位表示を保証します。この座標変換と代入は、実際に移動させた直後が理想です。
@onready var chara = $Sprite2D @onready var body = $CharacterBody2D func _physics_process(_delta) -> void: body.velocity = ... # 移動入力などの処理 body.move_and_slide() chara.global_position = body.global_position.round()
私は XeGrader plus では、DLC で MSX2 モードと ORIGIN(PC-8001mkII)モードを用意しています。どちらもレトロハードですから、移動に関してはバイト単位です※。MSX2 ではバイト単位だと SCREEN 5 で 2ドット単位になります。PC-8001mkII では 4ドット単位です。そこでこんな座標変換をしています。
# MSX2 chara.global_position = (body.global_position / 2).round() * 2 # PC-8001mkII chara.global_position = (body.global_position / 4).round() * 4
これで、正確なドット単位の移動が実現できます。
※私は検証にこれを使ってます。仕様はXInputなのに右側にAボタンという変態仕様なのですが、これにも XeGrader plus は対応しています。- 斜め移動でのカクつきを補正する
縦横の移動だと正確に 1ドット単位で動きます。ですが、斜め移動ではカクついた少々見苦しい表示になってしまいます。これはこの図を見てもらうのが一番分かりやすいでしょう。

これ、内部の座標が浮動小数点だからカクつくのです。このカクついた表示の状態をジャダー(階段状)と言い、太古の昔から忌み嫌われてきました(大袈裟)。これをなくすにはどうするか。答えは実にシンプルです。変換元の座標が整数なら良いのです。そもそもジャダーの発生原因は、小数点以下が存在するから起きています。カクつきを消すには、小数自体をなくすのが一番確実です。
そして、ここで残念なお知らせです。浮動小数が使えないということは、Godot の物理演算を使うことはできなくなるのです。XeGrader plus では移動先チェックに RayCast2D は使っていますが、動きに関しては全て自前で処理しています。その自前処理のために、プロパティとして次の変数を全てのキャラに共通で用意しています。
var vpos: Vector2i # 現在地 var dir: Vector2i # 移動方向 var speed: float # 移動速度(秒) var decimal: float # 蓄積タイマー
考え方としては固定小数です。decimal がアンダーフローしたら、桁が変わったと判断して vpos を移動します。この考え方は 8方向移動しか考慮されていません。理由も単純で、斜め方向移動のジャダーを防ぐには、X と Y を必ず同時に変化させることが必須だからです。ここまで言えば勘の良い人はもう分かっちゃいますよね。実装のフローは以下のようになります。
- dir に移動する方向を代入する
- decimal に speed を代入する
- decimal から delta を引く
- 結果が0以下になったら vpos に dir を加算して decimal に speed を足し戻す
ここでいくつか注意があります。まず、dir に格納する Vector2i ですが、X と Y の各要素は -1, 0, +1 のいずれかのみです。speed は小さいほど速く移動します。そして、丸く広がる移動を実現するために、方向が斜めであれば √2 を掛けた値を入れてください。例えば移動速度が 0.1 であれば、斜め方向の場合は 0.141421 がスピードとなります。なぜ √2 なのか知りたい人は、こちらを御覧ください。
『1』『2』の処理は、最初の一回と移動方向が変化する時のみ必要です。まっすぐ進むだけであれば、『3』『4』の処理を繰り返すだけとなります。以下に実装サンプルを提示します。
const SQRT2 = 1.41421356 var vpos: Vector2i # 現在地 var dir: Vector2i # 移動方向 var speed: float # 移動速度(秒) var decimal: float # 蓄積タイマー func _physics_process(delta: float) -> void: # 3. タイマー更新 decimal -= delta # 4. 0以下(時間経過)でドット移動 if decimal <= 0: vpos += dir var weight = SQRT2 if dir.x * dir.y != 0 else 1.0 decimal += speed * weight # Spriteに整数化した座標を渡す $Sprite2D.global_position = Vector2(vpos)この
この実装で確実な斜め移動が実現されました。ただ、これは折角の物理演算ノードである CharacterBody2D は単なる当たり判定用の入れ物になります。便利な処理系は一切使えません。正確なドット制御を優先するためのトレードオフです。どちらを採用するかは読者の判断にお任せしますが、少なくとも XeGrader plus では、このような実装を採用しています。
この記事が気に入って頂けたなら是非Wishlist登録をお願いいたします。
この記事が気に入って頂けたなら是非Wishlist登録をお願いいたします。


コメント