Attempt to using Nakama as replacement of Low-Level ENet

This commit is contained in:
2025-12-02 00:58:44 +08:00
parent b27b612989
commit ead155afed
74 changed files with 14205 additions and 23315 deletions
+47
View File
@@ -0,0 +1,47 @@
extends Node
# GameStateManager - Central game state management
signal game_started()
signal game_state_changed()
@export var enable_bots: bool = true
@export var max_players: int = 4
var players: Array = []
var bots: Array = []
var game_started_flag: bool = false
var local_player_character: CharacterBody3D
func is_game_started() -> bool:
return game_started_flag
func start_game():
game_started_flag = true
emit_signal("game_started")
func add_player(peer_id: int):
if not peer_id in players:
players.append(peer_id)
emit_signal("game_state_changed")
func remove_player(peer_id: int):
players.erase(peer_id)
emit_signal("game_state_changed")
func add_bot(bot_id: int):
if not bot_id in bots:
bots.append(bot_id)
players.append(bot_id)
emit_signal("game_state_changed")
func remove_bot(bot_id: int):
bots.erase(bot_id)
players.erase(bot_id)
emit_signal("game_state_changed")
func reset():
players.clear()
bots.clear()
game_started_flag = false
local_player_character = null
+40
View File
@@ -0,0 +1,40 @@
extends Node
# GoalManager - Manages goal generation and synchronization
var preset_goals: Array = []
func initialize_random_goals(size: int, min_value: int, max_value: int, null_count: float) -> Array:
var goals = []
var rng = RandomNumberGenerator.new()
rng.randomize()
var null_val = 0
var max_nulls = 3
const SPECIAL_VALUES = {1: 7, 2: 8, 3: 9, 4: 10}
for i in range(size):
if null_val < max_nulls and rng.randf() < null_count:
goals.append(-1)
null_val += 1
else:
var val = rng.randi_range(min_value, max_value)
goals.append(val if not val in SPECIAL_VALUES else SPECIAL_VALUES[val])
return goals
func generate_preset_goals(count: int) -> Array:
preset_goals.clear()
for i in range(count):
var goals = initialize_random_goals(9, 7, 10, 1.0)
var int_goals: Array[int] = []
for g in goals:
int_goals.append(g)
preset_goals.append(int_goals)
return preset_goals
func get_goals_for_player(player_index: int) -> Array:
if player_index >= 0 and player_index < preset_goals.size():
return preset_goals[player_index].duplicate()
return []
+56
View File
@@ -0,0 +1,56 @@
extends Node
# ObstacleManager - Handles obstacle placement and management
enum ObstacleOrientation {
NORTH = 0,
EAST = 1,
SOUTH = 2,
WEST = 3
}
var current_obstacle_orientation = ObstacleOrientation.NORTH
var current_obstacle_item = 12
var gridmap_ref # Reference to EnhancedGridMap
func initialize(gridmap):
gridmap_ref = gridmap
func place_obstacle(grid_position: Vector2i, local_player) -> bool:
if not local_player or local_player.action_points < 1:
return false
var floor_index = 3 # Always place on floor 3
var success = gridmap_ref.place_obstacle(
Vector3i(grid_position.x, floor_index, grid_position.y),
current_obstacle_item,
current_obstacle_orientation
)
if success:
local_player.action_points -= 1
return true
return false
func cycle_obstacle_orientation() -> String:
var orientations = [
ObstacleOrientation.NORTH,
ObstacleOrientation.EAST,
ObstacleOrientation.SOUTH,
ObstacleOrientation.WEST
]
var current_index = orientations.find(current_obstacle_orientation)
current_index = (current_index + 1) % orientations.size()
current_obstacle_orientation = orientations[current_index]
var direction_names = ["North", "East", "South", "West"]
return "Direction: " + direction_names[current_index]
func cycle_obstacle_type() -> String:
var obstacle_types = [12, 13, 14, 15]
var current_index = obstacle_types.find(current_obstacle_item)
current_index = (current_index + 1) % obstacle_types.size()
current_obstacle_item = obstacle_types[current_index]
return "Type: " + str(current_index + 1)
+38
View File
@@ -0,0 +1,38 @@
extends Node
# PlayerManager - Handles player and bot lifecycle management
signal player_added(peer_id, player_node)
signal player_removed(peer_id)
signal bot_created(bot_id, bot_node)
signal bot_removed(bot_id)
var player_scene = preload("res://scenes/player.tscn")
var connected_peer_ids = []
func add_player_character(peer_id: int, position: Vector2i = Vector2i.ZERO) -> Node:
var player_character = player_scene.instantiate()
player_character.set_multiplayer_authority(peer_id)
player_character.name = str(peer_id)
connected_peer_ids.append(peer_id)
return player_character
func create_bot(bot_id: int) -> Node:
var bot_character = player_scene.instantiate()
bot_character.set_multiplayer_authority(1) # Server controls bots
bot_character.name = str(bot_id)
bot_character.is_bot = true
return bot_character
func remove_player(peer_id: int):
connected_peer_ids.erase(peer_id)
emit_signal("player_removed", peer_id)
func get_next_available_bot_id(max_players: int, current_players: Array) -> int:
for i in range(2, max_players + 1):
if not i in current_players:
return i
return -1
+23
View File
@@ -0,0 +1,23 @@
extends Node
# TurnManager - Manages turn-based gameplay flow
signal turn_changed(player_id)
signal turn_ended()
var current_turn_index: int = 0
var turn_based_mode: bool = true
func next_turn(players: Array):
if turn_based_mode and players.size() > 0:
current_turn_index = (current_turn_index + 1) % players.size()
var next_player_id = players[current_turn_index]
emit_signal("turn_changed", next_player_id)
return next_player_id
return -1
func end_current_turn():
emit_signal("turn_ended")
func reset_turn():
current_turn_index = -1
+133
View File
@@ -0,0 +1,133 @@
extends Node
# UIManager - Handles all UI setup and updates
const item_tex = [
preload("res://assets/textures/player_board_and_blue_print/tile_null.tres"),
preload("res://assets/textures/player_board_and_blue_print/tile_heart.tres"),
preload("res://assets/textures/player_board_and_blue_print/tile_diamond.tres"),
preload("res://assets/textures/player_board_and_blue_print/tile_star.tres"),
preload("res://assets/textures/player_board_and_blue_print/tile_coin.tres")
]
# Node references - will be set by Main
var action_menu
var move_button
var grab_button
var put_button
var randomize_button
var arrange_button
var playerboard_ui
var local_player_character
enum ActionState {
NONE,
MOVING,
GRABBING,
PUTTING,
RANDOMIZING,
ARRANGING,
PLACING_OBSTACLE
}
var current_action_state = ActionState.NONE
func initialize(main_node):
# Get node references from main scene
action_menu = main_node.get_node("ActionMenu")
move_button = main_node.get_node("ActionMenu/ActionButtonContainer/MoveButton")
grab_button = main_node.get_node("ActionMenu/ActionButtonContainer/GrabButton")
put_button = main_node.get_node("ActionMenu/ActionButtonContainer/PutButton")
randomize_button = main_node.get_node("ActionMenu/ActionButtonContainer/RandomizeButton")
arrange_button = main_node.get_node("ActionMenu/ActionButtonContainer/ArrangeButton")
playerboard_ui = main_node.get_node("PlayerboardUI")
func setup_action_buttons(action_state_callback):
move_button.pressed.connect(func(): action_state_callback.call(ActionState.MOVING))
grab_button.pressed.connect(func(): action_state_callback.call(ActionState.GRABBING))
put_button.pressed.connect(func():
if local_player_character:
local_player_character.auto_put_item()
)
randomize_button.pressed.connect(func(): action_state_callback.call(ActionState.RANDOMIZING))
arrange_button.pressed.connect(func():
if local_player_character and local_player_character.action_points >= 2:
action_state_callback.call(ActionState.ARRANGING)
)
func setup_playerboard_ui():
for child in playerboard_ui.get_children():
child.queue_free()
playerboard_ui.columns = 5
for i in range(25):
var slot = TextureRect.new()
var highlight_rect = TextureRect.new()
var hr_tex = load("res://assets/models/pboard/HighlightRect.tres")
var select_rect = TextureRect.new()
var sr_tex = load("res://assets/models/pboard/SelectRect.tres")
var adjacent_rect = TextureRect.new()
var ar_tex = load("res://assets/models/pboard/AdjacentRect.tres")
slot.custom_minimum_size = Vector2(36, 36)
slot.texture = item_tex[0]
playerboard_ui.add_child(slot, true)
highlight_rect.texture = hr_tex
highlight_rect.size = Vector2(36, 36)
select_rect.texture = sr_tex
select_rect.size = Vector2(36, 36)
adjacent_rect.texture = ar_tex
adjacent_rect.size = Vector2(36, 36)
slot.add_child(highlight_rect)
slot.add_child(select_rect)
slot.add_child(adjacent_rect)
slot.get_child(0).hide()
slot.get_child(1).hide()
slot.get_child(2).hide()
func update_playerboard_ui():
if not local_player_character:
return
for i in range(25):
var slot = playerboard_ui.get_child(i)
var item = local_player_character.playerboard[i]
slot.texture = item_tex[0]
match item:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
func update_button_states():
if not local_player_character or local_player_character.is_in_group("Bots"):
move_button.visible = false
grab_button.visible = false
put_button.visible = false
randomize_button.visible = false
arrange_button.visible = false
return
move_button.visible = true
grab_button.visible = true
put_button.visible = true
randomize_button.visible = true
arrange_button.visible = true
move_button.disabled = false
grab_button.disabled = false
put_button.disabled = false
arrange_button.disabled = false
func set_local_player(player):
local_player_character = player
+111
View File
@@ -0,0 +1,111 @@
extends Node
# Standard Nakama Configuration
const NAKAMA_SERVER_KEY = "defaultkey"
const NAKAMA_HOST = "77.237.232.232"
const NAKAMA_PORT = 7350
const NAKAMA_SCHEME = "http"
# Core Nakama Variables
var client: NakamaClient
var session: NakamaSession
var socket: NakamaSocket
var bridge: NakamaMultiplayerBridge
# Signals
signal connected_to_nakama
signal connection_failed(error_message)
signal match_joined(match_id)
signal match_join_error(error_message)
# Current Match Info
var current_match_id: String = ""
func _ready():
# Initialize the Nakama Client
client = Nakama.create_client(NAKAMA_SERVER_KEY, NAKAMA_HOST, NAKAMA_PORT, NAKAMA_SCHEME)
# Ensure we process network events
set_process(true)
func _process(_delta):
# If using the standard socket adapter, it needs polling in some versions
if socket:
pass
func connect_to_nakama_async(email: String = "", password: String = "") -> void:
# 1. Authenticate
if email == "":
var device_id = OS.get_unique_id()
# For testing, append a random number to device ID to simulate unique users on one machine
# device_id = device_id + str(randi())
session = await client.authenticate_device_async(device_id)
else:
session = await client.authenticate_email_async(email, password)
if session.is_exception():
printerr("Auth Error: ", session.get_exception().message)
emit_signal("connection_failed", session.get_exception().message)
return
# 2. Connect Socket
socket = Nakama.create_socket_from(client)
var socket_result = await socket.connect_async(session)
if socket_result.is_exception():
printerr("Socket Error: ", socket_result.get_exception().message)
emit_signal("connection_failed", socket_result.get_exception().message)
return
# 3. Initialize Multiplayer Bridge
# This links Nakama's socket to Godot's High-Level Multiplayer API
bridge = NakamaMultiplayerBridge.new(socket)
# Connect bridge signals
bridge.match_joined.connect(_on_bridge_match_joined)
bridge.match_join_error.connect(_on_bridge_match_join_error)
# CRITICAL: Set Godot's multiplayer peer to the Nakama bridge
# This allows @rpc functions to work over Nakama
multiplayer.set_multiplayer_peer(bridge.multiplayer_peer)
print("Connected to Nakama and Bridge Initialized.")
emit_signal("connected_to_nakama")
# --- Match Management ---
func host_game():
if not bridge:
printerr("Cannot host: Bridge not initialized")
return
print("Hosting match via Nakama Bridge...")
var result = await bridge.create_match()
if result.is_exception():
emit_signal("match_join_error", result.get_exception().message)
func join_game(match_id: String):
if not bridge:
printerr("Cannot join: Bridge not initialized")
return
print("Joining match: ", match_id)
var result = await bridge.join_match(match_id)
if result.is_exception():
emit_signal("match_join_error", result.get_exception().message)
# --- Callbacks ---
func _on_bridge_match_joined() -> void:
current_match_id = bridge.match_id
print("Successfully joined match: ", current_match_id)
emit_signal("match_joined", current_match_id)
func _on_bridge_match_join_error(error) -> void:
printerr("Bridge failed to join match: ", error.message)
emit_signal("match_join_error", error.message)
func is_connected_to_nakama() -> bool:
return socket != null and socket.is_connected_to_host()
func _exit_tree():
if socket:
socket.close()
+1
View File
@@ -0,0 +1 @@
uid://doec8teq6vbhr
+16
View File
@@ -0,0 +1,16 @@
extends Node
# This script is deprecated in favor of NakamaManager.
# The logic has been moved to allow the NakamaMultiplayerBridge to handle
# the networking transparently.
func _ready():
pass
func host_game():
# Redirect to NakamaManager
NakamaManager.host_game()
func join_game(match_id):
# Redirect to NakamaManager
NakamaManager.join_game(match_id)
+1
View File
@@ -0,0 +1 @@
uid://l4qd7n8l2hch