Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 お絵かきパズル【第12回】
2022.05.13
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 お絵かきパズル【第12回】

目次

  • レベル画面
  • 背景・タイトル
  • 問題一覧
  • ドラッグスクロール処理
  • 進捗を消す
  • 問題を作る

レベル画面

下図にレベル画面のスクショを示す。

レベル画面は、アプリを起動すると最初に表示される画面だ。 上部にはタイトル等が表示され、中央部には縦スクロール可能な問題一覧が表示される。 そして下部には2つのボタンが配置されている。
以下、これらの実装方法について解説する。

■背景・タイトル

まずは、なにはともあれ背景だ。ColorRect を設置し、サイズを画面いっぱいにし、色を設定する。

タイトルは Label を設置し、ちょっと特殊なフォントを使用し、影付き文字として表現している。 タイトルロゴを作れる人は画像を配置してもよい。筆者には見栄えのよいロゴを自分で作るのは敷居が高いので、このようにしてみた。

タイトルフォントの設定は、下図のようにしている。Colors > Font Color Shadow をオンにし、 Fonts > Font を DynamicFont とし、サイズを指定し、Font > Font Data で TTF フォントを指定している。
これらの設定は初見では見つけるのが難しいかもしれないが、慣れればどうってことはないはずだ。

■問題一覧

ScrollContainer を設置し、その子ノードとして VBoxContainer を設置する。 そして、問題パネルインスタンスを生成し、その子ノードとすれば、自動的に垂直スクロールしてくれる。

各問題パネルの初期化コードは下記の通り。

var QuestPanel = load("res://QuestPanel.tscn")
func _ready():
    .....
    for qix in g.quest_list.size(): # 問題パネルセットアップ
        g.qix2ID[qix] = g.quest_list[qix][g.KEY_ID]
        var panel = QuestPanel.instance()       # 問題パネルインスタンス作成
        panel.set_number(qix+1)                 # 問題番号
        .....
        if g.solvedPat.has(g.qix2ID[qix]):      # クリア済み or 途中経過あり
            var lst = g.solvedPat[g.qix2ID[qix]]
            if lst.size() <= N_IMG_CELL_VERT || lst[N_IMG_CELL_VERT] > 0:
                solved = true
                panel.set_title(g.quest_list[qix][g.KEY_TITLE])
            else:
                panel.set_title(g.quest_list[qix][g.KEY_TITLE][0] + "???")
            panel.set_ans_image(lst)
            if lst.size() > N_IMG_CELL_VERT:    # クリアタイムあり
                panel.set_clearTime(lst[N_IMG_CELL_VERT])
                if solved:
                    if lst[N_IMG_CELL_VERT] < diffi * 60 * 0.5:
                        ns = 3
                    elif lst[N_IMG_CELL_VERT] < diffi * 60:
                        ns = 2
                    elif lst[N_IMG_CELL_VERT] < diffi * 60 * 2:
                        ns = 1
            else:
                panel.set_clearTime(0)
        else:
            panel.set_title(g.quest_list[qix][g.KEY_TITLE][0] + "???")
            panel.set_clearTime(0)
        if solved:
            nSolved += 1
            score += diffi * (10 + ns*2)
        panel.set_star(ns)
        panel.set_author(g.quest_list[qix][g.KEY_AUTHOR])
        $ScrollContainer/VBoxContainer.add_child(panel)     # パネルインスタンスをコンテナに追加
        # ボタン押下時シグナルをハンドラに接続
        panel.connect("pressed", self, "_on_QuestPanel_pressed")

各ボタンは QuestPanel という名前で別シーンとして作成しているので、instance() でインスタンス化し、 問題番号等を設定し、add_child() で垂直方向コンテナに追加している。

問題がクリア済み、または途中経過が保存されている場合は、問題タイトルを設定し、 panel.set_ans_image() を呼んで、イラストのサムネイル表示を行う。set_ans_image() の実装は次章で解説する。
問題が未クリアの場合は、ネタバレにならないようにタイトルの先頭文字+"???" をタイトルとして表示している。

次に、問題ボタンが押された場合に呼ばれるハンドラのコードを示す。

func _on_QuestPanel_pressed(num):
    if mouse_pos == null || dialog_opened:      # ダイアログがオープン状態の場合など
        return
    var v = $ScrollContainer.scroll_vertical
    g.lvl_vscroll = $ScrollContainer.scroll_vertical    # スクロール位置保存
    g.solveMode = true;         # 問題を解くモード
    g.qNumber = num             # 問題番号
    get_tree().change_scene("res://MainScene.tscn")     # パズルを解くメインシーンに遷移

このシーンに戻ってきたときのために、スクロール位置を保存し、モードを設定し、クリックされた問題の番号をグローバル変数に保存し、 get_tree().change_scene() をコールしてパズルを解くメインシーンに遷移している。

■ドラッグスクロール処理

マウスボタンが押下されると、下記の _input(event) がコールされる。

func _input(event):
    if event is InputEventMouseButton:
        if event.is_action_pressed("click"):        # left mouse button
            if $ScrollContainer.get_global_rect().has_point(event.position):        # in ScrollContainer
                mouse_pushed = true;
                mouse_pos = event.position
                scroll_pos = $ScrollContainer.get_v_scroll()    # 押下時点でのスクロール位置を保存
        elif event.is_action_released("click"):
            mouse_pushed = false;
            mouse_pos = null
    elif event is InputEventMouseMotion && mouse_pushed:    # mouse Moved
        $ScrollContainer.set_v_scroll(scroll_pos + mouse_pos.y - event.position.y)

_input(event) はキー押下などでもコールされるので、最初に「event is InputEventMouseButton」 でマウスボタンイベントかどうかをチェックする。
次に「event.is_action_pressed("click")」で押下されたかどうかをチェックし、 そうであれば、スクロールコンテナ内がクリックされた場合は mouse_pushed フラグを true にし、ドラッグモードにする。

マウスボタンがリリースされた場合は、event.is_action_released("click") が真になる。 その場合は、mouse_pushed フラグを false にする。

マウスが押下されたまま移動、すなわちドラッグされた場合は「event is InputEventMouseMotion && mouse_pushed」が真になる。 この場合は ScrollContainer.set_v_scroll() をコールし、ドラッグ量に応じた垂直スクロールを行う。

なお、問題パネル押下検出処理は、問題パネルクラスの方で行い、pressed() シグナルを発行するようになっている。 その説明は次章で行う。

■進捗を消す

【進捗を消す】が押下された場合に呼ばれるハンドラの実装を以下に示す。

func _on_ClearButton_pressed():
    $ClearProgressDialog.window_title = "SakuSakuLogic"
    $ClearProgressDialog.dialog_text = "Are you sure to clear Progress ?"
    $ClearProgressDialog.popup_centered()
    dialog_opened = true

確認ダイアログは「ClearProgressDialog」というノード名でノードツリーに追加されているので、 タイトルとテキストを設定し、popup_centered() をコールし、画面中央に表示する。

確認ダイアログで【OK】が押下されると _on_ClearProgressDialog_confirmed() がコールされる。 この接続はエディタの右側のインスペクタで「ノード」を選び、confirmed(), popup_hide() シグナルを、 それぞれの処理ハンドラに接続しておく(下図参照)。

_on_ClearProgressDialog_confirmed() の実装は下記の通り。

func _on_ClearProgressDialog_confirmed():
    g.solvedPat = {}
    var dir = Directory.new()
    dir.remove(g.solvedPatFileName)     # 進捗データファイルを消去
    #
    for i in g.quest_list.size():   # 問題パネルセットアップ
        var qix = i
        var panel = $ScrollContainer/VBoxContainer.get_child(i)
        panel.set_title(g.quest_list[qix][g.KEY_TITLE][0] + "???")
        panel.set_clearTime(0)
        panel.set_ans_image([])
        #panel.update()
    $scoreLabel.text = "SCORE: 0"
    $solvedLabel.text = "Solved: 0/%d (0%)" % g.quest_list.size()

ダイアログが閉じられると _on_ClearProgressDialog_popup_hide() がコールされるので、 下記の様に、ダイアログオープンフラグをオフにしておく。

func _on_ClearProgressDialog_popup_hide():
    dialog_opened = false

■問題を作る

【問題を作る】ボタンが押されると、_on_EditButton_pressed() がコールされる。

func _on_EditButton_pressed():
    g.lvl_vscroll = $ScrollContainer.scroll_vertical
    g.solveMode = false;
    get_tree().change_scene("res://MainScene.tscn")

戻ってきたときのために、スクロール位置をグローバル変数に保存し、 問題を解くモードかどうかのフラグをオフにし、 get_tree().change_scene(シーン名) でシーンを切り替えているだけだ。