前回Godotのチュートリアルにある簡単なゲームを作成しました。

今回は、Godotのプロジェクト一覧からダウンロードできるテンプレートプロジェクト「2D Role Playing Game Demo」の中身を読んでみます。

参考ページ:

環境:

  • Godot Engine v3.2.2

ゲームの機能。

  • キーボード操作でマップを徘徊
  • 対面するオブジェクト毎に違うアクションが発生する
  • 会話Windowが表示され会話ができる
  • 対戦モードがある

学びメモ。

Gameシーン

  • AnimationPlayer: 画面切り替えアニメーション
  • Transition(CanvasLayer):
    • ColorRect: fadeにりようする黒塗りNode
  • Combat: 対戦シーン
  • Exploration: 探索シーン
  • Camera2D: カメラ
extends Node

# Nodeのファイルパスを紐づけるために公開変数にする
export(NodePath) var combat_screen
export(NodePath) var exploration_screen

# ファイルパスの定義
const PLAYER_WIN = "res://dialogue/dialogue_data/player_won.json"
const PLAYER_LOSE = "res://dialogue/dialogue_data/player_lose.json"

func _ready():
  # Nodeの取得
	exploration_screen = get_node(exploration_screen)
	combat_screen = get_node(combat_screen)

  # シグナルの接続 .connect(<signal_name>, <target_node>, <target_function_name>)
	combat_screen.connect("combat_finished", self, "_on_combat_finished")
  # $Exploration は get_node( "Exploration") の短縮系
	for n in $Exploration/Grid.get_children():
		if not n.type == n.CellType.ACTOR:
			continue
		if not n.has_node("DialoguePlayer"):
			continue
  # モブのシグナルの接続 
		n.get_node("DialoguePlayer").connect("dialogue_finished", self,
			"_on_opponent_dialogue_finished", [n])
  # 戦闘シーン削除
	remove_child(combat_screen)


# 戦闘開始
func start_combat(combat_actors):
	# 探索シーン削除
	remove_child($Exploration)
	# fadeイン
	$AnimationPlayer.play("fade")
	
	# animation_finished のシグナル後再開
	yield($AnimationPlayer, "animation_finished")
	# 戦闘シーン追加
	add_child(combat_screen)
	# 戦闘シーン表示
	combat_screen.show()
	# 戦闘シーン初期化
	combat_screen.initialize(combat_actors)
	# fadeアウト
	$AnimationPlayer.play_backwards("fade")


# 対戦相手の会話が終了
func _on_opponent_dialogue_finished(opponent):
	# 対戦相手が負けていたらreturn 
	if opponent.lost:
		return
	# 対戦ペアを決めて戦闘開始
	var player = $Exploration/Grid/Player
	var combatents = [player.combat_actor, opponent.combat_actor]
	start_combat(combatents)


# 対戦終了
func _on_combat_finished(winner, _loser):
	# 対戦シーンを削除
	remove_child(combat_screen)
	# fadeアウト
	$AnimationPlayer.play_backwards("fade")
	# 探索シーン追加
	add_child(exploration_screen)
	# 対話シーン読み込み
	var dialogue = load("res://dialogue/dialogue_player/DialoguePlayer.tscn").instance()
	# 会話ファイル設定
	if winner.name == "Player":
		dialogue.dialogue_file = PLAYER_WIN
	else:
		dialogue.dialogue_file = PLAYER_LOSE
	# アニメーション終了後再会
	yield($AnimationPlayer, "animation_finished")
	# ダイアログを表示
	var player = $Exploration/Grid/Player
	exploration_screen.get_node("DialogueUI").show_dialogue(player, dialogue)
	# 対戦シーンクリア
	combat_screen.clear_combat()
	# ダイアログが終了後再会
	yield(dialogue, "dialogue_finished")
	# ダイアログを解放
	dialogue.queue_free()

Combatシーン

  • Combatants (Node2D): 戦闘員の配置View
  • TurnQueue: ターンを管理
  • UI (Control)
    • Combatants (HboxContainer): 戦闘員のゲージ配置View
    • Buttons (PanelContainer): コマンドView
      • GridContainer: コマンドガイドコンテナ
        • Attack (Button): Attackボタン
        • Defend (Button): Defendボタン
        • Flee (Button): Fleeボタン
extends Node

# 戦闘終了のシグナル
signal combat_finished(winner, loser)
# 戦闘員の型?
const Combatant = preload("res://turn_combat/combatants/Combatant.gd")

# 初期設定
func initialize(combat_combatants):
	# 渡された戦闘メンバー
	for combatant in combat_combatants:
		combatant = combatant.instance()
		if combatant is Combatant:
			# CombatantsNodeに追加
			$Combatants.add_combatant(combatant)
			# ConbatantのHealtnNodeのdeadシグナルの接続
			combatant.get_node("Health").connect("dead", self, "_on_combatant_death", [combatant])
		else:
			combatant.queue_free()
	$UI.initialize()
	$TurnQueue.initialize()


# 状態クリア
func clear_combat():
	# CombatantsとUI/Combatantsの子Node解放
	for n in $Combatants.get_children():
		n.queue_free()
	for n in $UI/Combatants.get_children():
		n.queue_free()


# 戦闘を終了する
func finish_combat(winner, loser):
	emit_signal("combat_finished", winner, loser)


# 戦闘で倒れた通知
func _on_combatant_death(combatant):
	var winner
	if not combatant.name == "Player":
		winner = $Combatants/Player
	else:
		for n in $Combatants.get_children():
			if not n.name == "Player":
				winner = n
				break
	finish_combat(winner, combatant)

Explorationシーン

  • Grass (TileMap): マス目
  • Grid (TileMap): オブジェクトマップ
    • Player (Node2D): プレイヤー
    • Opponent (Node2D): 対戦相手
      • DialoguePlayer: ダイアログ情報
      • Timer: (?)
    • Object (Node2D): アイテム
      • DialoguePlayer: ダイアログ情報
  • DialogueUI (Panel): ダイアログ
extends TileMap

# Enum定義
enum CellType { ACTOR, OBSTACLE, OBJECT }
# Nodeのファイルパス
export(NodePath) var dialogue_ui

func _ready():
	# 子NodeをCellとして配置
	for child in get_children():
		set_cellv(world_to_map(child.position), child.type)


# Cellから対象のポーンを取得
func get_cell_pawn(cell, type = CellType.ACTOR):
	for node in get_children():
		if node.type != type:
			continue
		if world_to_map(node.position) == cell:
			return(node)


# ポーンの移動要求
func request_move(pawn, direction):
	var cell_start = world_to_map(pawn.position)
	var cell_target = cell_start + direction
	
	var cell_tile_id = get_cellv(cell_target)
	match cell_tile_id:
		-1: # 空きだったら
			# 移動
			set_cellv(cell_target, CellType.ACTOR)
			set_cellv(cell_start, -1)
			return map_to_world(cell_target) + cell_size / 2
			
		CellType.OBJECT, CellType.ACTOR: # オブジェクトがあったら
			# 移動先にあったポーン取得
			var target_pawn = get_cell_pawn(cell_target, cell_tile_id)
			print("Cell %s contains %s" % [cell_target, target_pawn.name])
			
			if not target_pawn.has_node("DialoguePlayer"): # ダイアログ要素がなければ
				return
			
			# ダイアログ表示
			get_node(dialogue_ui).show_dialogue(pawn, target_pawn.get_node("DialoguePlayer"))