【Godot4.x】スイカゲームのアレンジサンプル

スイカゲームのアレンジサンプルを作ったので、簡単に解説する記事となります。

スイカゲームのアレンジサンプル

ソースコード

ソースコードはGitHubにアップロードしています。

MITライセンスなので、基本的に自由に使ってもらって問題ありません。

それと一部の文字にドット風の「マルミーニャ」というフォントを使っています。

こちら無料で使えるフォントですが、気に入ったらBOOTHから支援することもできます。

実行ファイル

Windows版の実行ファイルは以下からダウンロードできます。

データ・モジュール構造

データ・モジュールの構造は以下のようになっています。

データ・モジュール構造
res://
 +-- assets
 |    +-- fonts: フォントデータ
 |    +-- images: 画像データ
 |    |    +-- fruits: フルーツ画像
 |    |    +-- bg.png: 背景画像
 |    |    +-- deadline.png: ゲームオーバーの線
 |    |    +-- gauge_progress.png: ゲームオーバーゲージ
 |    |    +-- gauge_under.png: ゲームオーバーゲージ背景
 |    |    +-- line.png: 落下補助線
 |    |
 |    +-- sound: サウンドデータ
 |    |    +-- bgm: BGM
 |    |    +-- se: 効果音
 |    |
 |    +-- fruit_physics_material.tres: フルーツ物理パラメータ
 |
 +-- src: ソースコード
 |    +-- common: 共通
 |    |    +-- Common.gd: 共通スクリプト
 |    |    +-- Ease.gd: イージングスクリプト
 |    |
 |    +-- fruit: フルーツ
 |    |    +-- Fruit*.tscn: フルーツシーン
 |    |    +-- Fruit.gd: フルーツスクリプト
 |    |
 |    +-- particle: パーティクル
 |    |    +-- ParticleScore.gd: スコア演出スクリプト
 |    |    +-- ParticleScore.tscn: スコア演出シーン
 |    |    +-- ParticleUtil.gd: パーティクルユーティリティ
 |    |
 |    +-- Wall.tscn: 壁シーン
 |
 +-- default_bus_layout.tres: オーディオレイアウト
 +-- Main.gd: メインシーンスクリプト
 +-- Main.tscn: メインシーン

 

シンプルなゲーム内容と比べてモジュール数が少し多いのは、オリジナルのスイカゲームを何回か遊んでみて、自分だったらこういう修正やルールを入れたいな…というアレンジが入っているためです。

なおオリジナルに忠実で、わかりやすいチュートリアルが必要である場合には「logic-lab」さんの「スイカゲームっぽいゲームを作ろう【GodotEngine】」というチュートリアル動画がおすすめです。

アレンジ概要

今回のサンプルのアレンジポイントは以下の通りです。

  • 1. ゲームオーバーのラインを超えても即死しないようにした
  • 2. スコア演出を入れた
  • 3. BGMの演出を入れた

1. ゲームオーバーのラインを超えても即死しない

今回の一番大きな変更点がこれです。

オリジナル版では、オブジェクトが一瞬でもライン超えしたら即死する、というルールのため想定外の物理挙動によってオブジェクトが飛び跳ねてラインを超えてしまうと即死…、というやや理不尽な現象がありました。

そのため、今回のアレンジではゲージを表示して、3秒間ライン超えしないとゲームオーバーにならない…というルールに修正しています。

個人的にはこれが一番フェアなのではないかと思って実装しましたが、実際に作ってみるとこの修正によって失われるメリットもある(「難易度の低下」「(突然の死で)実況動画映えしなくなる」)ので、「ライン超え=即死」が一概に間違ったゲームデザインであるは言えない部分があるのかも…という気もしています。

2. スコア演出を入れた

スイカゲームは基本的にスコアを競うゲームと考えています。そう考えたときにスコアの主張がやや弱いように感じたので、スコアが加算されるタイミングでスコア演出を入れてみました

スコアの表示でオブジェクトが少し隠れてしまうのが気になるデメリットですが、スコアへの意識が上がって修正前よりも良くなったような気がします。ただスイカゲームでは、1回の進化に対するスコアの値は小さいので、演出以上の意味があまりないかもしれません…。

3. BGMの演出を入れた

ゲームの進行に合わせてBGMを変化させたいなぁ…と思って、進化で登場するフルーツに合わせてBGMを変化させるようにしました。また死亡ラインを超えているフルーツがある場合にはBGMのピッチを下げたりとインタラクティブな要素を若干入れています。

フルーツ(Fruit)の実装について

フルーツの実装は「src/fruit」にあります。

src/fruit
src/fruit
     +-- Fruit.gd: フルーツスクリプト
     +-- Fruit.tscn
     +-- FruitBullet.tscn: のどあめシーン
     +-- FruitCarrot.tscn: 人参シーン
     +-- FruitRadish.tscn: 大根シーン
     +-- FruitPocky.tscn: ポッキーシーン
     +-- FruitBanana.tscn: バナナシーン
     +-- FruitNasu.tscn: なすシーン
     +-- FruitTako.tscn: たこシーン
     +-- FruitNya.tscn: にゃーシーン
     +-- Fruit5Box.tscn: 5箱シーン
     +-- FruitMilk.tscn: 牛乳シーン
     +-- FruitPudding.tscn: プリンシーン
     +-- FruitXBox.tscn: XBoxシーン

フルーツの挙動はフルーツの種類によって変化しないため、共通のスクリプト「Fruit.gdをそれぞれのシーンにアタッチして実装しています。

もともとスイカゲームのオブジェクトはすべて球体なのですが、このサンプルでは画像にあわせてコリジョンの形状をそれぞれのフルーツで変化させています。例えばバナナはカプセルコリジョンを2つ組み合わせています。

それと各フルーツシーンに共通の物理パラメータとして「fruit_physics_material.tres」を設定しています。変更箇所は「Friction」の値を「0.5」にしています。これはオリジナルのスイカゲームの物理が摩擦少なめでスルスルと落ちていく動きをしていたので、摩擦係数をデフォルトの「1.0」から「0.5」に減らしています。

フルーツスクリプト(Fruit.gd)の実装について

余計な実装が多いのでやや長めのスクリプトですが、スイカゲームを作るときに主要となる部分は以下のところです。

  • 1. Rigidbody2Dを継承している
  • 2. body_enteredシグナルを有効にしている

スイカゲームは物理演算を使った挙動となるので、Godot標準の物理エンジンを使うと楽に実装できます。物理挙動を行うオブジェクトを作るには “Rigidbody2D” を使います。

それと衝突判定はbody_entered」シグナルを利用しています。

## 他の剛体と衝突した.
func _on_body_entered(body: Node) -> void:
	# ヒット回数をカウント.
	_hit_count += 1
	
	if not body is Fruit:
		return # フルーツでない
	if self.is_queued_for_deletion():
		return # すでに破棄要求されている.
	if body.is_queued_for_deletion():
		return # すでに破棄要求されている.
		
	# フルーツとヒットした.
	var other = body as Fruit
	if id != other.id:
		return # 一致していないので何も起こらない.
	
	# IDが一致していたら合成可能.
	if id < eFruit.XBOX:
		# とりあえず中間地点にフルーツを生成する.
		var pos = (position + other.position)/2
		# このシグナル内で生成する場合は
		# 遅延処理をしなければならない.
		var is_deferred = true
		# 進化するのでid+1
		var fruit = Common.create_fruit(id+1, is_deferred, pos)
		fruit.position = pos
		fruit.start_scale()
	else:
		# XBox同士は消せない.
		return
	
	# お互いに消滅する.
	queue_free()
	other.queue_free()

ただ Rigidbody2D の衝突シグナルはデフォルトで無効となっています。これを有効にするにはインスペクタから Contact Monitor” をオンにして、Max Contacts Reported” の値を1以上にする必要があります。

もう1つのハマりどころとしては、このシグナルは相互に処理されます。例えばなす同士が衝突した場合は両方にシグナルが発生してしまいます。

そのため同じフルーツが衝突したときに進化後のオブジェクトをお互いが生成すると、進化後のオブジェクト生成され続ける問題が発生します。

それを回避しているのが “queued_for_deletion()” でこれを呼び出すと消滅処理が呼び出されているかどうかを判定できます。すでに消滅処理 (queue_free)が呼び出されている場合は、マージ(結合)処理が終わっているはずなので、何もせずに return します。

ゲームオーバー判定

ゲームオーバー判定は各Fruitオブジェクトがゲームオーバーのラインを超えているときに “_gameover_timer” に経過時間が加算され、それが一定の値を超えるとゲームオーバーという処理となっています。

Main.gd _is_gameover() がゲームオーバーかどうかを判定する関数となります。

## ゲームオーバーかどうか.
func _is_gameoveer(delta:float) -> bool:
	for obj in _fruit_layer.get_children():
		var fruit = obj as Fruit
		if fruit.check_gameover(_deadline.position.y, delta):
			# ゲームオーバー猶予時間を超えた.
			return true
	
	# ゲームオーバーでない.
	return false

ここで Fruitオブジェクトの _check_gameover() を呼び出して、ゲームオーバーのラインの高さと経過時間を渡しています。

## ゲームオーバーのラインを超えているかどうか.
func check_gameover(y:float, delta:float) -> bool:
	if is_hit_even_once() == false:
		return false # ヒットしていなければ対象外.
		
	if position.y < y:
		# ライン超えしている
		# 猶予時間.
		_gameover_timer += (delta * 2)
		if _gameover_timer > TIMER_GAMEOVER:
			# ライン超え.
			return true
	
	# セーフ.
	return false

経過時間「delta」を2倍にして足し込んでいるのは、_phsics_process() で経過時間を差し引いているためです。

## 更新.
func _physics_process(delta: float) -> void:
	
	# 拡大縮小演出.
	_spr.scale = _base_scale
	if _scale_timer > 0:
		_scale_timer -= delta
		var rate = _scale_timer / TIMER_SCALE
		rate = 1 + 0.1 * Ease.back_out(1 - rate)
		_spr.scale *= Vector2(rate, rate)
	
	# ゲームオーバー赤点滅演出.
	_spr.modulate = Color.WHITE
	if _gameover_timer > 0:
		var rate = 1 - (_gameover_timer / TIMER_HIT)
		_gameover_timer -= delta
		var color = Color.RED
		_spr.modulate = color.lerp(Color.WHITE, rate)

円ゲージの実装

ライン超えしたときの円ゲージは “TextureProgressBar” ノードを使用しています。

“Fill Mode” を “Clockwise” (時計回り)、“Texture” の “Under”と”Over” に円形のテクスチャを指定すると円のゲージが作れます。

スコアの計算

スイカゲームにおけるスコアの配点は以下のようになっています。

フルーツ点数増分
イチゴ 1点+1
ブドウ 3点+2
デコポン 6点+3
ミカン 10点+4
リンゴ 15点+5
ナシ 21点+6
モモ 28点+7
パイナップル 36点+8
メロン 45点+9
スイカ 55点+10

数学に慣れている方ならすぐにわかりそうですが、1つの進化ごとに1点増えていく…という計算となります。

ということで Common.gd add_score() に実装したスコア加算処理は以下のようになります。

## スコア加算.
## @return 加算したスコアの値.
func add_score(id:Fruit.eFruit) -> int:
	# スコアの式は Σ(n-1)らしい....
	var v = 0
	for i in range(id, 0, -1):
		v += i
	score += v
	if score > hi_score:
		hi_score = score # ハイスコア更新.
	
	return v