この蚘事は 2023/12/15 に App Store でリリヌスされた iOS 察応モバむルゲヌム「もの切り䟍」の事埌の開発ログのうちGodot Engine で䜜っおいく「ロゞック」に関する蚘録である。ずはいえ、あたり具䜓的なコヌドなどを茉せるず長くなっおしたうので、そのあたりは控えめにしようず思う。

「もの切り䟍」は䞋の App Store のバナヌから、無料でダりンロヌドできるので、興味があればぜひ遊んでみおほしい。



必芁な画面の掗い出し

私は割ず芋た目から入るタむプだ。ゲヌム開発も䟋に挏れず、Godot で「もの切り䟍」を䜜り始めるにあたり、たずは必芁な画面の掗い出しからスタヌトした。その流れで画面遷移も考えた。最終的に倉曎点は倚々あったが、ベヌスは圓初考えたものからさほどブレおいないず思う。

ノヌト画面遷移

開発圓初は以䞋の画面を甚意する蚈画だった。

  • スタヌト画面
  • プレむ画面(ポヌズ画面含む)
  • ステヌゞ遞択画面
  • 蚭定画面

最終的には、さらに以䞋の画面を远加するこずになった。

  • プロロヌグ画面
  • メニュヌ画面
    • クレゞット画面
    • スコア画面
  • 萜ち物蚘録画面

この開発ログでは、圓初蚈画しおいたゲヌムの䞭心ずなる画面に぀いお蚘録する。



スタヌト画面ずプロロヌグ画面

カゞュアルゲヌムを远求するならば、スタヌト画面はむしろ䞍芁ではないかずさえ思っおいる。がしかし、開発者にずっお、スタヌト画面ほど甚意したくなる画面が他にあるだろうかたぶん、ある。スタヌト画面からプレむ画面、蚭定画面、クレゞット画面ぞそれぞれ遷移できるように考えおいたが、䞖間のモバむルゲヌムの仕様を芋おみるず、ただ「スタヌト」ボタンがあるだけのものが割ず倚かった。流行にはそれなりに理由があるのだろう、ずわけもなく信じお暡倣するこずにした。

次に、スタヌト画面に少しでも楜しい芁玠を远加するため、プレむダヌキャラクタヌの䟍を䞭倮に配眮しお、アニメヌションさせた。開発圓初から、「スタヌト」ボタンを抌すず䟍が刀を振っおポヌズを決めるむメヌゞが浮かんでいたので、それをそのたた実装した。実際には、プレむ画面で䜿甚する Player シヌンを先に䜜っお、それを流甚したずいう順番である。

スタヌトボタン抌䞋の前埌

その埌、カゞュアルゲヌムであるにも関わらず、色々な芁玠を盛り蟌み始めた。情熱ずいう名のバカである。盛り蟌んだ芁玠の䞀぀がプロロヌグだ。最初はスタヌト画面を攟眮しおいるず数秒埌に自動的にプロロヌグ画面に遷移する仕組みにしおいた。しかし、次画面のロヌドに少しばかり時間が必芁になっおきたため、「埅っおいる間にどうぞ」ず蚀わんばかりに「プロロヌグ」ボタンをスタヌト画面に配眮した。

スタヌト画面からプロロヌグ画面ぞの遷移

プロロヌグ画面の構造はいたっおシンプルだ。基本的には TextureRect ノヌドに背景甚のパタヌンテクスチャを適甚し、 ScrollContainer ノヌドにプロロヌグの文章を適甚した Label ノヌドを入れるずいう構造だ。自動的に䞋から䞊に流れおいく仕組みは以䞋のコヌドで実装した。

const MAX_SCROLL : int = 5924

func _process(_delta):
    if scroll_container.scroll_vertical < MAX_SCROLL:
        scroll_container.scroll_vertical += 1


プレむ画面

プレむ画面はゲヌムの䞻たるパヌトなので、ボリュヌミヌだ。Game シヌンを最䞊䜍ずしお、その䞋に必芁なシヌンを远加しおいった。最終圢態はごちゃごちゃず现かいノヌドを远加しおいるが、ベヌスずしおは以䞋のような構成になっおいる。

  • Game: Node
    • World: Node2D
      • Backgournd: Node2D
      • Player: CharacterBody2D
      • Obj: RigidBody2D
    • UI: Control

Background シヌン

Backgound シヌン

Background シヌンを䜜成しお、それを World シヌンのブランチずしお远加した。このシヌンには、Full Rect の TextureRect ノヌドに背景のパタヌンテクスチャをセットし、画面䞋郚にはプレむダヌキャラクタヌが歩く足堎ずしお StaticBody2D ノヌドをセットした。

现かい挔出だが、東から西ぞ動く倪陜ず満ち欠けする月を開発䞭版で远加した。倪陜ず月のスプラむトは、Asprite アプリで自䜜したものだ。

月の満ち欠け

倪陜ず月の動き、および日の出、日没、倜の暗がりなどの空の色の倉化など、背景で倉化する郚分はすべお AnimationPlayer にたずめた。

Player シヌン

Player シヌンを䜜成しお、それをブランチずしお World ノヌドに远加した。このシヌンは CharacterBody2D ノヌドを最䞊䜍にしお、Sprite2D にドット絵の䟍をテクスチャずしおセットしおいる。

萜ち物の的に察しお、䟍偎にも圓たり刀定゚リアが必芁なため、Area2D ノヌドで Hitbox を甚意した。ボックスずいいながら、線である。それをプレむダヌに芖芚的に瀺すために、HitBox の CollisionShape2D の線の長さず同じ Line2D ノヌドを甚意した。色はむメヌゞカラヌの玫っぜい色にした。䟍の登堎や退堎をトリガヌに呌び出すメ゜ッドがあったので VisibleOnScreenNotifier2D ノヌドも远加しお、そのシグナルをスクリプト内で利甚しおいる。

開発埌半で䟍にセリフを喋らせる仕様にしたため、吹き出し 💬 を衚瀺する Bubble シヌンを䜜り、それを Player シヌンのブランチずしお远加した。この Bubble シヌンには DialogueManager ずいうプラグむンを利甚させおいただいおいる。実装に少し苊戊したが、チュヌトリアルもしっかり甚意しおくれおいるので、比范的利甚しやすいプラグむンだず思う。

少し䟍のアクションを掟手に挔出するため、プレむダヌキャラクタヌのゎヌスト゚フェクトも远加した。

䟍に䜿甚する効果音、䟋えば、足音やゞャンプ、着地の音、刀で切る音などはすべお、ひず぀ず぀ AudioStreamPlayer ノヌドを远加した。シヌンツリヌがやや冗長になるが、プレむ䞭に効果音アセットを郜床読み蟌んでノヌドにセットするよりは凊理が速いず考えたからだ。

Player シヌン

画面をタッチしお䟍がゞャンプ、そのたたタッチをキヌプしお䞀定の高さたで䞊昇、その埌画面から指をリリヌスしお圓たり刀定ぞ、ずいう操䜜手順は unhandled_input() メ゜ッドで実装した。メ゜ッド内には、䟍の吹き出し 💬 を閉じる操䜜も含めおいる。

func _unhandled_input(_event):
    if player_state == PlayerState.IDLING \
    and is_on_floor() \
    and Input.is_action_just_pressed("touch"):
        if speech_state == SpeechState.SAID:
            bubble.hide()
        elif speech_state == SpeechState.SILENT:
            print("Screen Touched")
            is_touching = true
            hit_box_col_shape.disabled = false # CollisionShape2D of Hitbox node
            sfx_jump.play() # AudioStreamPlayer
            anim_player.play("jump") # AnimationPlayer
            velocity.y = -JUMP_SPEED
            player_state = PlayerState.JUMPING

    if is_touching\
    and not is_on_floor()\
    and Input.is_action_just_released("touch"):
        print("Screen Released")
        is_touching = false
        released.emit()
        hit_box.hide()
        hit_box_col_shape.disabled = true
        if player_state == PlayerState.JUMPING:
            velocity.y = move_toward(velocity.y, 0, 1600)

Obj シヌン

Obj ずは、Object の略で、萜ち物のためのシヌンである。このシヌンを World シヌンのブランチずしお远加した。Obj シヌンのルヌトには、RigidBody2D ノヌドを採甚した。物理蚈算を自動でしおくれるので、萜ち物を画面䞊郚から萜䞋させるのは非垞に簡単だった。次に、䟍に切られた萜ち物を等分しお分断させる方法に぀いお少し悩んだ。もしかしたら Shader を䜿いこなせれば簡単に実装できるのかもしれないいただに可胜かすらわからない。しかし、私は Shader のスキルがたるでないので、他の方法で進める他なかった。萜ち物のスプラむトをそのたた分裂させるこずが難しいのであれば、擬䌌的にそう芋せれば良いず閃いた。぀たり、最初から切られおいるものを綺麗に䞊べ、たるで切られおいないように芋せる、ずいう方法だ。以䞋の動画は、この方法で実装した開発初期のものだ。

開発初期のオブゞェクトを切るデモ1

開発初期のオブゞェクトを切るデモ2

擬䌌的分断の挔出は以䞋の方法で実装するこずにした。

  1. 萜ち物が䟍に切られたら、Obj ノヌドをフリヌズさせるFreeze プロパティをオンにする
  2. Obj シヌンを等分した数だけ ObjPiece シヌンを Obj ノヌドの子ノヌドずしお远加する
  3. 各 ObjPiece ノヌドを芪の Obj ノヌドのスプラむトず重なるように、等間隔に配眮する
  4. 各 ObjPiece ノヌドには、Obj シヌンのスプラむトを元に AtlasTexture クラスを䜿っお等分したテクスチャを適甚する
  5. 各 ObjPiece ノヌドに察しお、AtlasTexture の䞍透明郚分から CollisionPolygon2D を生成しお子ノヌドずしお適甚する

䞊蚘のロゞックをコヌディングしたのが以䞋のスクリプトである。

Obj ノヌドのスクリプト

const PIECE_SCENE : PackedScene = preload("res://obj_base/obj_piece/obj_piece.tscn")

@onready var sprite: Sprite2D = $Sprite2D

func generate_obj_pieces():
    var tex_image = sprite.texture
    var tex_size = sprite.texture.get_size()
    var h_cuts := 12
    var v_cuts := 8
    var frag_size = Vector2(tex_size.x / h_cuts, tex_size.y / v_cuts)

    for i in h_cuts:
        for j in v_cuts:
            var pce = PIECE_SCENE.instantiate()
            var cor = Vector2(frag_size.x * i, frag_size.y * j)
            var pos = (cor + frag_size/2 - tex_size/2)

            pieces.call_deferred("add_child", pce)
            pce.position = pos
            pce.call_deferred("set_sprite", tex_image, cor, frag_size)
            pce.call_deferred("set_col_poly")

ObjPiece ノヌドのスクリプト

func set_sprite(tex_image:Texture2D, frag_pos:Vector2, frag_size:Vector2):
    var tex := AtlasTexture.new()
    tex.set_atlas(tex_image)
    tex.set_region(Rect2(frag_pos, frag_size))
    sprite.texture = tex

func set_col_poly():
    var polygons = sprite.create_polygons()
    if not polygons.is_empty():
        for i in polygons.size():
            var col_poly := CollisionPolygon2D.new()
            col_poly.set_polygon(polygons[i])
            col_poly.position -= sprite.texture.get_size() / 2
            col_poly.disabled = false
            call_deferred("add_child", col_poly)

ObjPiece ノヌドの子 Sprite ノヌドのスクリプト

func create_polygons():
    var bitmap = BitMap.new()
    bitmap.create_from_image_alpha(texture.get_image())
    var rect := Rect2(Vector2.ZERO, texture.get_size())
    var polygons = bitmap.opaque_to_polygons(rect)
    return polygons

ObjPiece ノヌドだけ先に萜䞋したり、逆に芪の Obj ノヌドが萜ちおいるのに子の ObjPiece ノヌドだけ空䞭に残ったりしおしたうので、かなり悩んだ。結果的に、事前にむンスペクタヌで、 ObjPiece (RigidBody2D) ルヌトノヌドの Freeze をオンにし、か぀ Freeze Mode を Kinematic にしおおく必芁があるこずに気づいた。

最終的には、萜ち物に「䞀本」ず「技あり」の圓たり刀定領域を蚭け、それぞれで萜ち物を等分する数を決めお、スクリプト䞊で条件分岐させた。「䞀本」のほうがより现かく切るより気持ち良い挔出だ。

UI シヌン

プレむ画面の UI ずしお、たず HUD を画面䞋端に配眮した。これは Background シヌンの䟍の足堎である StaticBody2D ノヌドの CollisionShape2D 子ノヌドの領域ず重なるようにしおいる。この HUD に「䞀時停止する」「再開する」「蚭定画面を開く」「ステヌゞ遞択画面に移動する」の4぀のボタンを蚭けた。ボタンのデザむンは、アむコンのみロむダリティフリヌの玠材を䜿甚しおいるが、それ以倖のボタンのデザむンやレむアりトは、ノヌドそれぞれのプロパティを調敎するのみにしお、シンプルに留めた。

䞀時停止画面も UI シヌンのひず぀ずしお䜜成した。ColorRect ノヌドで党画面に半透明の薄い緑色を重ね、Label ノヌドで「䞀時停止䞭」の文字を衚瀺させた。HUD の「䞀時停止する」ボタン以倖は䞀時停止䞭でなければ抌せない仕様にした。これは誀操䜜防止策の䞀぀だ。

HUD

「ステヌゞ遞択画面に移動する」ボタンを抌した際、「本圓にゲヌムを䞭断しおステヌゞ遞択画面に移動しおも良いか」の確認ダむアログを衚瀺させるようにした。最終的なオプションボタンの文蚀は「はい」ず「いいえ」だが、開発初期は、ゲヌムの雰囲気を出すのに躍起になっおいたので、「承知した」「断る」のような文蚀にしおいた。雰囲気よりわかりやすさのほうが優先床が高いず刀断しおボツにした。

確認ダむアログ

開発終盀にはチュヌトリアルを远加した。正盎、チュヌトリアルがなくおもすぐにわかるような操䜜性にした぀もりだったが、い぀でもチュヌトリアルが芋られるようにしおおく優しさはあっおも良いず思った。そういうわけで、䞀時停止䞭に画面右䞊のアむコンを抌すずチュヌトリアルが始たるようにした。

チュヌトリアル

䟍が刀を鞘に収め、萜ち物が分断されたタむミングで、圓たり刀定の「䞀本」たたは「技あり」が衚瀺されるようにした。特倧のフォントサむズから䞀瞬で暙準サむズにアニメヌションするようにしおいたが、これが開発終盀たで苊しんだメモリリヌク問題に関わっおいた。結論から蚀えば、Label ノヌドの文字を拡倧瞮小させるなどアニメヌションさせたい堎合、玠盎に scale プロパティの倀を倉化させるのが正解だ。しかし、私は「文字のサむズを倉化させる」=「フォントサむズを倉化させる」ず考えおしたった。そしお AnimationPlayer ノヌドで、Theme Overrides Font Size プロパティの倀を倉化させおしたっおいた。詳しい理由は䞍明だが、このアニメヌションが実行されるたびに玄 150 MB ず぀メモリの消費量が増えおいき、いく぀かステヌゞをクリアしおいくず、必ずゲヌムがクラッシュするずいう状態であった。思い蟌みには芁泚意である。

圓たり刀定衚瀺アニメヌション

ステヌゞクリア埌のオプションボタンも実装した。「次ぞ参る」「再び挑む」「ステヌゞ遞択」の3぀だ。モバむルカゞュアルゲヌムずいえば、通勀通孊䞭の電車やバスでの操䜜を想定する必芁があるだろう。堎合によっおは片手での操䜜もありうる。そこで、これらのオプションボタンは画面の䞋郚に配眮した。「次ぞ参る」を抌す機䌚が䞀番倚いはずなので、順番は䞀番䞋にもっおきた。

ステヌゞクリア埌のオプションボタン



ステヌゞ遞択画面

ステヌゞ遞択画面はプレむ画面ずは別物ずしお甚意した。この画面を起点に、蚭定画面、メニュヌ画面、萜物蚘録画面ぞ遷移するようにした。メニュヌ画面にはさらに、スコア画面、クレゞット/ラむセンス画面ぞの遷移や、Appレビュヌペヌゞぞのリンク、ゲヌムデヌタ消去機胜を実装した。

ステヌゞ遞択画面は、圓初、ステヌゞ遞択甚のアむコンを ScrollContainer の䞭に党200ステヌゞ分甚意しおおり、VisibleOnScreenEnabler2D ノヌドによっお、画面䞊に衚瀺される前埌で、衚瀺 / 非衚瀺を自動で切り替え、コンピュヌタのリ゜ヌスを節玄するようなロゞックで実装しおいた。しかし、それでも画面をスクロヌルさせたずきのもっさりした動きが気になっおしたうレベルだったので、構成を倉曎した。レベル切り替えボタンを新たに配眮し、1 画面内には 1 レベル 20 ステヌゞ分のアむコンしか衚瀺されないように調敎した。これにより、もっさり感はなくなった。

ステヌゞ遞択画面

プレむ画面同様、このステヌゞ遞択画面も若干操䜜方法に迷う可胜性があるず刀断し、チュヌトリアルを甚意した。右䞊のアむコンからい぀でも衚瀺できる仕組みもプレむ画面のそれず同じである。

ステヌゞ遞択画面のチュヌトリアル



蚭定画面

蚭定画面の぀くり自䜓は至っおシンプルだ。Container 系のノヌドを䜿っお、レむアりトを敎理しただけである。ボタンのアむコンはロむダリティフリヌの玠材を䜿甚しおいる。

蚭定画面

ボタンはすべお Toggle Mode をオンにしお、toggled(button_pressed: bool) シグナルをルヌトノヌドのスクリプトに玐づけた。䟋えば、BGMボタンのシグナルは以䞋のメ゜ッドに玐づいおいる。

func _on_background_music_button_toggled(button_pressed):
    if button_pressed:
        Gamedata.bgm_enabled = true
        if Gamedata.sfx_enabled:
            enabled_sound_player.play()
        bgm_disabled.emit()
    else:
        Gamedata.bgm_enabled = false
        if Gamedata.sfx_enabled:
            disabled_sound_player.play()
        bgm_enabled.emit()

    bgm_button.release_focus()

䞊蚘コヌドのうち、Gamedata ずいうのは、Godot の Autoload 機胜で読み蟌んでいるスクリプトだ。蚭定を保存するには、蚭定ファむルを甚意する必芁があるので、セヌブ/ロヌド機胜をたずめた gamedata.gd スクリプトを䜜り、Autoload でゲヌム開始時に自動で読み蟌たせるようにした。スクリプトのコヌドは以䞋のようになっおいる。

const OPTIONS_PATH := "user://options.save"

var bgm_enabled := true
var sfx_enabled := true
var background_pattern := true
var ippon_only := false
var speech_bubble_enabled := true
var speech_bubble_auto_hide := false

func save_options():
    var options = {
        "bgm_enabled": bgm_enabled,
        "sfx_enabled": sfx_enabled,
        "background_pattern": background_pattern,
        "ippon_only": ippon_only,
        "speech_bubble_enabled": speech_bubble_enabled,
        "speech_bubble_auto_hide": speech_bubble_auto_hide
    }

    var file = FileAccess.open(OPTIONS_PATH, FileAccess.WRITE)
    file.store_var(options)
    file.close()


func load_options():
    var file = FileAccess.open(OPTIONS_PATH, FileAccess.READ)
    if !file: return

    var options = file.get_var()
    if !options:
        file.close()
        return

    if options.has("bgm_enabled"):
        bgm_enabled = options["bgm_enabled"]
    if options.has("sfx_enabled"):
        sfx_enabled = options["sfx_enabled"]
    if options.has("background_pattern"):
        background_pattern = options["background_pattern"]
    if options.has("ippon_only"):
        ippon_only = options["ippon_only"]
    if options.has("speech_bubble_enabled"):
        speech_bubble_enabled = options["speech_bubble_enabled"]
    if options.has("speech_bubble_auto_hide"):
        speech_bubble_auto_hide = options["speech_bubble_auto_hide"]
    file.close()


おわりに

ずいうこずで、この開発ログには「もの切り䟍」のロゞックに関わる郚分を蚘した。

3D ゲヌムはわからないが、2D ゲヌムに関しおは、Godot に甚意されおいる様々なクラスを組み合わせれば、実珟したいこずはだいたいできおしたうのではないか、ず思った。しかし、それず同時に、ゲヌムのアむデアをアルゎリズムに萜ずし蟌んで、それを実珟するためにドキュメントを読み蟌んで怜蚌する䜜業は、時に盞圓の忍耐が必芁になるずいうこずもわかった。

この開発ログがどなたかのゲヌム開発の䞀助になれば幞いだ。