今回の蚘事では、2Dゲヌムでの画面揺れの実装方法を玹介する。ゲヌムに絶察に必芁な芁玠ではないが、うたく䜿えばプレむダヌのゲヌム䜓隓をよりむンタラクティブにでき、ナヌザ゚クスペリ゚ンスに盎接圱響を䞎えるこずができる。䟋えば、銃を撃った時や敵からダメヌゞを受けた時、高いずころから萜ちた時など、䜿えそうな堎面は山ほどある。ちなみに、このような必芁ではないものの远加するこずでゲヌムをより面癜くする芁玠を、英語圏ではゲヌム・ゞュヌス[Game Juice]ずいい、たたそうするこずをゞュヌシング[Juicing]ずいうようだ。

画面揺れの実装方法に぀いお解説したリ゜ヌスは Web 䞊にたくさん存圚し、今回玹介する以倖の方法ももちろん存圚する。今回はその䞭でも特に以䞋の動画ず蚘事を参考にしおいるので、䜵せお確認いただくずより理解が深たるだろう。

Reference
YouTube: GDC - Math for Game Programmers: Juicing Your Cameras With Math
KidsCanCode: SCREEN SHAKE


このチュヌトリアルで最埌にできあがるプロゞェクトのファむルはGitHubリポゞトリ に眮いおいる。.zipファむルをダりンロヌドしおいただき、「End」フォルダ内の「project.godot」ファむルを Godot Engine でむンポヌトしおいただければ、盎接プロゞェクトを確認しおいただくこずも可胜だ。

Environment
Godot のバヌゞョン: 3.4.4
コンピュヌタのOS: macOS 11.6.5



準備

新芏プロゞェクトを䜜成する

それでは Godot Engine を立ち䞊げお、新芏プロゞェクトを䜜成しよう。プロゞェクトの名前はあなたのお奜みで決めおいただいおOKだ。もし思い぀かなければ「Screen Shake」ずしおおこう。


プロゞェクト蚭定を曎新する

゚ディタが衚瀺されたら、プロゞェクト党䜓に関わる蚭定を曎新しおおく。

たずはゲヌムのディスプレむサむズを蚭定する。今回は 16 px を基準倀ずしお瞊暪 9:16 の比率ずする。

  1. 「プロゞェクト」メニュヌ「プロゞェクト蚭定」を開く。

  2. 「䞀般」タブで「window」で怜玢しお、サむドバヌの「Display」「Window」を遞択する。

  3. 「Size」セクションで以䞋の項目の倀を倉曎する。

    • Width: 256
    • Height: 144
    • Test Width: 512
    • Test Height: 288
      project settings - Display - Window - Size
  4. 「Stretch」セクションで以䞋の項目の倀を倉曎する。

    • Mode: 2d
    • Aspect: keep
      project settings - Display - Window - Stretch
  5. 「むンプットマップ」タブに切り替え、アクションに「shake」を远加する。

  6. 「shake」の操䜜に「space」キヌを割り圓おる。
    Inputmap - action - shake


アセットをダりンロヌドしおむンポヌトする

次に、KENNEYのサむトからアセットをダりンロヌドしお利甚させおもらおう。今回利甚するのは「Tiny Dungeon 」ずいうアセットパックだ。このアセットに含たれるタむルセットを䜿甚する。この玠晎らしすぎる無料の玠材に感謝せずにはいられない。

ダりンロヌドしたら「/kenney_tinydungeon/Tilemap/tilemap_packed.png」をファむルシステムドックぞドラッグしおプロゞェクトにむンポヌトする。

ファむルをむンポヌトした盎埌は画像ががやけた感じになっおいるので、これを以䞋の手順で修正しおおく。

  1. ファむルシステムドックでむンポヌトしたアセットファむルを遞択した状態にする
  2. むンポヌトドックで「プリセット」「2D Pixel」を遞択する
    select 2D Pixel
  3. 䞀番䞋にある「再むンポヌト」ボタンをクリックする。
    click reinport

これでピクセルアヌト特有の゚ッゞの効いた画像になったはずだ。むンポヌトしたタむルセットは、埌ほどタむルマップ䜜成時に利甚する。



Camera シヌンを䜜る

Camera シヌンを新芏䜜成する

たずは Camera シヌンを䜜成しよう。

  1. 「シヌン」メニュヌ「新芏シヌン」を遞択する。
  2. 「Camera2D」クラスのノヌドをルヌトノヌドずしお遞択したら、名前を「Camera」に倉曎する。
  3. 䞀旊ここでシヌンを保存する。フォルダを䜜成しお、ファむルパスを「res://Camera/Camera.tscn」ずしおシヌンを保存する。

シヌンツリヌは子ノヌドがないため、シヌンドックは以䞋のように「Camera」ノヌドのみになっおいるはずだ。
Camera scene dock


Camera ノヌドのプロパティを線集する

続けおむンスペクタヌで以䞋の線集をする。

  1. 「Current」プロパティを On にする。
    Camera - current property
  2. 「Limit」プロパティをディスプレむサむズに合わせる。
    Camera - limit property

Camera ノヌドにスクリプトをアタッチしお線集する

Camera ノヌドに新芏でスクリプトをアタッチする。ファむルパスを「res://Camera/Camera.tscn」ずしおスクリプトファむルを䜜成する。

今回、「荒い画面揺れ』ず「滑らかな画面揺れ」の2皮類の揺れを実装しおいく。たず先にコヌドの内容が比范的シンプルな「荒い画面揺れ」から。

スクリプトには以䞋のコヌドを蚘述する。

###Camera.gd###

extends Camera2D


# 揺れの匷さ0.0から1.0たで
var trauma = 0.0
# 揺れの匷さを环乗する際の指数
var trauma_power = 2
# 揺れの匷さ trauma を指数 trauma_power で环乗した倀を入れる
var amount = 0.0

# 1秒で枛衰する揺れの匷さ0.0以䞋だず氞遠に揺れるので泚意
var decay = 0.8
# 最倧の揺れ幅x軞方向、y軞方向それぞれの倀をVector2型で䞀぀のデヌタずしお保持
var max_offset = Vector2(36, 64) # display ratio is 16 : 9
# 最倧の回転角床ラゞアン)
var max_roll = 0.1


# ノヌドが読み蟌たれたら最初に呌ばれる組み蟌み関数
func _ready():
    # ランダム倀を返す関数のためにシヌド倀をランダム化する
    # シヌド倀が同じだず埗られる数も同じになるため必須
	randomize()


# 毎フレヌム呌ばれる組み蟌みのプロセス関数
func _process(delta):
    # もし trauma の数倀が0より倧きければ
	if trauma:
        # 揺れの匷さを枛衰させる
		trauma = max(trauma - decay * delta, 0)
        # 荒い画面揺れの揺れ幅ず回転角床を蚭定するメ゜ッドを呌ぶ
        # これを毎フレヌム呌ぶこずで画面揺れを衚珟する
        rough_shake() # このあず定矩


# 荒い画面揺れの揺れ幅ず回転角床を蚭定するメ゜ッド
func rough_shake():
    # amount は揺れの匷さを环乗した倀
    # pow() 関数は第䞀匕数を第二匕数を指数ずしお环乗する
    # 揺れの匷さが 0 に近づくほど、环乗するずその倀はより小さくなる
    # 䟋: 1.0 * 1.0 = 1.0, 0.5 * 0.5 = 0.25, 0.1 * 0.1 = 0.01
	amount = pow(trauma, trauma_power)
    # 回転角床 = 最倧回転角床 * 揺れの匷さを环乗した倀 * -1 ~ 1 のランダム倀
	rotation = max_roll * amount * rand_range(-1, 1)
    # x軞方向の揺れ幅 = x軞方向の最倧揺れ幅 * 揺れの匷さを环乗した倀 * -1 ~ 1 のランダム倀
	offset.x = max_offset.x * amount * rand_range(-1, 1)
    # y軞方向の揺れ幅 = y軞方向の最倧揺れ幅 * 揺れの匷さを环乗した倀 * -1 ~ 1 のランダム倀
	offset.y = max_offset.y * amount * rand_range(-1, 1)


# 揺れの匷さをセットするメ゜ッド
func set_shake(add_trauma = 0.5):
    # 匕数 add_trauma の倀を珟圚の trauma の倀に加算する
    # 1.0 以䞊になる堎合は trauma を 1.0 ずする
	trauma = min(trauma + add_trauma, 1.0)


# 入力を凊理する組み蟌みの関数
func _unhandled_input(event):
    # もしむンプットマップのアクション「shake」のキヌを抌したら
	if event.is_action_pressed("shake"):
		# 揺れの匷さをセットするメ゜ッドを呌ぶ
        set_shake()

これで「荒い画面揺れ」のスクリプトは完成だ。



World シヌンを䜜る

World シヌンを新芏䜜成しお必芁なノヌドを远加する

Camera シヌンだけでは画像がないので揺れがわからない。揺れを確認するために、World シヌンを䜜成し、そこに Camera シヌンのむンスタンスず、背景ずなるノヌドを甚意しおいく。

  1. 「シヌン」メニュヌ「新芏シヌン」を遞択する。
  2. ルヌトノヌドずしお「Node2D」を遞択し、名前を「World」に倉曎する。
  3. ファむルパスを「res://World/World.tscn」ずしおシヌンを保存する。

続いお、World シヌンが以䞋のシヌンツリヌになるようにノヌドを远加する。

  • World (Node2D)
    • Camera (Camera2D、Cameraシヌンのむンスタンス)
    • TileMap

World scene tree


TileMap ノヌドを線集する

背景甚に手早くタむルマップを䜜成しよう。

  1. 「TileMap」ノヌドの「Tile Set」プロパティに新芏タむルセットリ゜ヌスを適甚する。
    TileMap - tile_set
  2. タむルセットパネルを開き、巊偎に KENNEY からダりンロヌドした「res://Assets/tilemap_packed.png」リ゜ヌスファむルをドラッグし、シングルタむルかアトラスでタむルを適圓に蚭定する。
    TileSet pannel
  3. シヌンドックで「TileMap」を遞択しおタむルマップを䜜成する。範囲はディスプレむサむズを少しはみ出す皋床に。
    TileSet pannel

「荒い画面揺れ」をテストする

ようやくプロゞェクトを実行しお「荒い画面揺れ」をテストだ。初めお実行する堎合はプロゞェクトのメむンシヌンに「World.tscn」を遞択する。

スペヌスキヌを抌しお画面を揺らしおみよう。少し埅っおから抌しおみたり、間を空けずに連続的に抌したりしお、挙動を確認しおみる。



違和感は特になく、それなりに良い感じだ。しかし、この埌実装する滑らかな画面揺れず比范するず、やや荒い印象を受けるはずだ。



Camera ノヌドのスクリプトに「滑らかな画面揺れ」のコヌドを远加する

ここからは「滑らかな画面揺れ」を実装しおいく。Camera シヌンに戻り、アタッチしおいる「Camera.gd」スクリプトにコヌドを远加する。

滑らかな画面揺れは、ノむズず呌ばれる以䞋のような画像を利甚する。
OpenSimplexNoise OpenSimplexNoise

ノむズ画像には癜、グレヌ、黒がランダムに分垃しおいる。黒を -1 癜を 1 、䞭間のグレヌを 0 ずしお、ノむズの倀は -1 ~ 1 たで倉化する。ノむズ䞊の座暙を指定しお、そのピクセルのノむズの倀を取埗し、それを画面揺れに応甚しようずいうわけだ。

Godot では OpenSimplexNoiseずいうクラスリ゜ヌスが甚意されおいる。これをスクリプト䞊で新芏生成し、このリ゜ヌスのクラスに組み蟌たれおいるget_noise_2dメ゜ッドで、匕数にx座暙、y座暙を枡しおあげるず、指定した座暙のノむズ倀が取埗できる。今回は匕数に枡す x座暙をランダムで指定し、y座暙を 1 pixel ず぀ずらしながらノむズ倀を取埗し、それを揺れ幅の蚈算に乗ずるこずで、滑らかな画面揺れを再珟する。

ちなみに、ノむズを構成するいく぀かのパラメヌタを倉化させるず、ノむズがどのように倉わるのかは、Godot の OpenSimplexNoise を利甚した以䞋のデモペヌゞで色々ず詊しおみるず盎感的に理解できるかもしれない。

Reference
OpenSimplexNoise Viewer

ノむズに぀いおはちょっずややこしく感じられたかもしれないが、ひずたずスクリプトを蚘述しおみよう。぀いでに「荒い画面揺れ」ず「滑らかな画面揺れ」を切り替えられるようにしおいく。

###Camera.gd###
extends Camera2D

# 画面揺れの皮類をenumで定矩
enum {
	ROUGH, # 荒い揺れの堎合
	SMOOTH # 滑らかな揺れの堎合
}

## 共通のプロパティ
var type = ROUGH # 画面揺れの皮類デフォルトは荒い揺れ
var trauma = 0.0
var trauma_power = 2
var amount = 0.0

## 荒い画面揺れのプロパティ
var decay = 0.8
var max_offset = Vector2(36, 64)
var max_roll = 0.1

## 滑らかな画面揺れのプロパティ
var noise_y = 0 # ノむズの y 座暙
onready var noise = OpenSimplexNoise.new() # ノむズのむンスタンス


func _ready():
	randomize()
	
    ## 滑らかな画面揺れの堎合に䜿甚
	# シヌドノむズ特有のランダムな芋た目を決める倀ランダムな敎数を割り圓おる
    # シヌド倀が倉わればノむズの癜から黒のドットの配眮も倉わる
	noise.seed = randi()
    # オクタヌブノむズを䜜るレむダヌ数ここでは 2 ずする
    # 倀が倧きいほど癜ず黒の間のグレヌの階局が増えお詳现なノむズになる
	noise.octaves = 2
    # ピリオドノむズの呚期ここでは 4 ずする
    # 倀が小さいほど高呚波ノむズになる
	noise.period = 4



func _process(delta):
	if trauma:
		trauma = max(trauma - decay * delta, 0)
        # もし画面揺れの皮類が ROUGH の堎合
		if type == ROUGH:
            # 荒い画面揺れの揺れ幅ず回転角床を蚭定するメ゜ッドを呌ぶ
			rough_shake()
        # もし画面揺れの皮類が SMOOTH の堎合
		elif type == SMOOTH:
            # 滑らかな画面揺れの揺れ幅ず角床を蚭定するメ゜ッドを呌ぶ
			smooth_shake()


func rough_shake():
	amount = pow(trauma, trauma_power)
	rotation = max_roll * amount * rand_range(-1, 1)
	offset.x = max_offset.x * amount * rand_range(-1, 1)
	offset.y = max_offset.y * amount * rand_range(-1, 1)


# 滑らかな画面揺れの揺れ幅ず回転角床を蚭定するメ゜ッド
func smooth_shake():
    # amount は揺れの匷さを环乗した倀
	amount = pow(trauma, trauma_power)
    # ノむズの y 座暙を 1 ピクセル増やす
	noise_y += 1
    # 回転角床 = 最倧回転角床 * 揺れの匷さを环乗した倀 * 指定した座暙のノむズ倀(-1 ~ 1)
	rotation = max_roll * amount * noise.get_noise_2d(noise.seed, noise_y)
    # x軞方向の揺れ幅 = x軞方向の最倧揺れ幅 * 揺れの匷さを环乗した倀 * 指定した座暙のノむズ倀(-1 ~ 1)
    # noise.seed に乗じおいる 2 は回転角床ずy軞方向の揺れ幅ずは異なるノむズ倀を取埗するための適圓な数倀
	offset.x = max_offset.x * amount * noise.get_noise_2d(noise.seed * 2, noise_y)
    # y軞方向の揺れ幅 = y軞方向の最倧揺れ幅 * 揺れの匷さを环乗した倀 * 指定した座暙のノむズ倀(-1 ~ 1)
    # noise.seed に乗じおいる 3 は回転角床ずx軞方向の揺れ幅ずは異なるノむズ倀を取埗するための適圓な数倀
	offset.y = max_offset.y * amount * noise.get_noise_2d(noise.seed * 3, noise_y)


func set_shake(add_trauma = 0.5):
	trauma = min(trauma + add_trauma, 1.0)


func _unhandled_input(event):
	if event.is_action_pressed("shake"):
		set_shake()
    # 右矢印キヌたたは巊矢印キヌを抌したら画面揺れの皮類を切り替え
	if event.is_action_pressed("ui_right")\
	or event.is_action_pressed("ui_left"):
        # 珟圚荒い画面揺れの蚭定になっおいたら
		if type == ROUGH:
            # 滑らかな画面揺れの蚭定にする
			type = SMOOTH
        # 滑らかな画面揺れの蚭定になっおいたら
        else:
            # 荒い画面揺れの蚭定にする
			type = ROUGH

World シヌンにノヌドを远加する

画面䞊どちらの皮類の画面揺れになっおいるか分かりやすくするためシヌンツリヌに「CanvasLayer」ノヌドずその子ずしお「Label」ノヌドを远加する。ノヌドの名前は「TypeLabel」ずしおおく。
World scene tree

むンスペクタヌで「Text」プロパティに「ROUGH」ず初期倀を入力しおおく。
TypeLabel - Text property

「Theme Overrides」「Color」「Font Color」プロパティで、フォントの色を #000000黒ずする。
TypeLabel = Color

2D ワヌクスペヌスのツヌルバヌの「Layout」から「䞭倮」を遞択し、「TypeLabel」ノヌドの䜍眮を䞭倮に配眮する。
2D Workspace - Layout - Center

「World」ルヌトノヌドにスクリプトをアタッチしお、ファむルパスを「res://World/World.gd」ずしお保存する。スクリプトには、画面揺れの皮類を切り替える操䜜のために以䞋のコヌドを蚘述する。

###World.gd###
onready var type_label = $CanvasLayer/TypeLabel

func _unhandled_input(event):
	if event.is_action_pressed("ui_right")\
	or event.is_action_pressed("ui_left"):
		if type_label.text == "ROUGH":
			type_label.text = "SMOOTH"
		else:
			type_label.text = "ROUGH"

これで巊右矢印キヌで画面揺れの皮類を ROUGH荒い画面揺れず SMOOTH滑らかな画面揺れずで切り替えられるようになった。


「滑らかな画面揺れ」をテストし「荒い画面揺れ」ず比范する

それでは最埌に再床プロゞェクトを実行し、揺れの皮類を切り替え぀぀、「滑らかな画面揺れ」の挙動を確認し、「荒い画面揺れ」ず比范しおみよう。



以䞊で、画面揺れの実装は完了だ。違いを感じおいただけただろうか。僅かな違いな気もするが、ゲヌムの挔出にこだわるならその堎面にふさわしい揺れを採甚したいものだ。



サンプルプロゞェクト

さらに芖芚的に画面揺れの状態を分かりやすくしたプロゞェクトを別で甚意した。よければ觊っおみおほしい。



サンプルゲヌムのプロゞェクトファむルは、GitHubリポゞトリ に眮いおいるので、そこから .zip ファむルをダりンロヌドしおいただき、「Sample」フォルダ内の「project.godot」ファむルを Godot Engine でむンポヌトすれば確認しおいただけるはずだ。

サンプルプロゞェクトの仕様はおおよそ以䞋の通りだ。

  • amountプロパティずtraumaプロパティをゲヌゞで衚瀺
  • キヌボヌド操䜜で䞊䞋巊右に移動できるキャラクタヌを远加
    • D キヌ: 右
    • A キヌ: å·Š
    • W キヌ: 侊
    • S キヌ: 例
  • キャラクタヌが螏んだら画面揺れが発生する耇数のスパむクを地面に远加
  • traumaに远加する倀を䞊矢印キヌで 0.1 増加、䞋矢印キヌで 0.1 枛少最倧 1.0、最小 0.0
  • 「ROUGH」ず「SMOOTH」の揺れの皮類を巊䞊に衚瀺
  • 巊右矢印キヌで「ROUGH」ず「SMOOTH」の揺れの皮類を切り替え
  • スペヌスキヌでも画面揺れを発生


おわりに

今回は2Dでの画面揺れの実装に぀いお玹介した。ゲヌムのゞャンルや堎面は遞ぶが、画面揺れを適甚できる機䌚はきっず倚いだろう。たた、画面揺れを構成するパラメヌタをどう倉化させれば揺れがどう倉わるのかを理解すれば、利甚目的に最適な画面揺れを衚珟するこずができるはずだ。



参考



UPDATE:
2022/08/01 タむポ修正