1
Fork 0
mirror of https://github.com/Steffo99/nanogolf.git synced 2024-10-16 06:17:36 +00:00

Everything works!

This commit is contained in:
Steffo 2024-03-15 19:57:15 +01:00
parent c252fe13fc
commit ff9336eecd
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
15 changed files with 239 additions and 73 deletions

View file

@ -23,6 +23,12 @@ window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/handheld/orientation=1
[layer_names]
2d_physics/layer_1="Default"
2d_physics/layer_2="Players"
2d_physics/layer_3="Walls"
[physics]
common/physics_ticks_per_second=300

View file

@ -7,4 +7,4 @@ class_name FollowCamera
func _physics_process(_delta):
if target != null:
position = target.position
position = target.global_position

View file

@ -2,31 +2,58 @@ extends CharacterBody2D
class_name GolfBall
@export_category("References")
## The [PuttController] that this node should poll.
@export var putt_controller: PuttController
## The [HoleController] that this node should poll.
@export var hole_controller: HoleController
## The [AudioStreamPlayer2D] that this node should play when entering the hole.
@export var hole_sound: AudioStreamPlayer2D
## The [Label] where the name of this player should be displayed.
@export var player_label: Label
@export_category("Physics")
## Dynamic friction subtracted from the body's velocity on each physics step.
@export var physics_friction: float
## The maximum number of bounces that the collision algorithm will consider in a single physics step.
@export var physics_max_bounces: float
## A multiplier applied to the body's velocity every time it collides with something.
@export var physics_bounce_coefficient: float
## The scene to instantiate to play the collision sound
@export var collision_sound: PackedScene
@export_category("Sounds")
## Curve mapping relative putt power to putt sound volume.
@export var collision_volume_db: Curve
## The velocity at which the maximum volume of [member collision_volume_db] is played.
@export var collision_volume_max_velocity: float
## Emitted when the ball enters the hole.
signal entered_hole(strokes: int)
## How many strokes have been performed so far.
var strokes: int = 0
## Whether the ball has entered the hole.
var in_hole: bool = false
@onready var putt_controller: PuttController = $"PuttController"
@onready var hole_controller: HoleController = $"HoleController"
@onready var hole_sound: AudioStreamPlayer2D = $"HoleSound"
@onready var player_label: Label = $"PlayerLabel"
## The name of the player represented by this scene.
var player_name: String = "Player":
@ -88,25 +115,36 @@ func do_movement(delta: float) -> void:
velocity *= physics_bounce_coefficient
func apply_friction(delta) -> void:
## Reduce [field velocity] by [field physics_friction], taking the [param delta] into account.
func apply_friction(delta: float) -> void:
var new_velocity_length = max(0.0, velocity.length() - physics_friction * delta)
velocity = velocity.normalized() * new_velocity_length
## Return whether this object can be considered stopped or not.
func check_has_stopped() -> bool:
return velocity.length() == 0.0
## Return whether this object will enter the hole on this frame or not.
func check_has_entered_hole() -> bool:
return check_has_stopped() and hole_controller.over_hole
func enter_hole() -> void:
@rpc("authority", "call_local", "reliable")
func rpc_sync_enter_hole():
in_hole = true
visible = false
hole_sound.play()
entered_hole.emit(strokes)
print("[GolfBall] Entered hole in: ", strokes)
Log.peer(self, "Entered hole in: %d" % strokes)
# FIXME: What happens on the server?
## Push this object's [field global_position] to all other clients.
@rpc("authority", "call_remote", "unreliable_ordered")
func rpc_sync_global_position(nposition: Vector2):
global_position = nposition
func _ready() -> void:
@ -114,10 +152,12 @@ func _ready() -> void:
func _physics_process(delta) -> void:
if not in_hole:
do_movement(delta)
apply_friction(delta)
if check_has_entered_hole():
enter_hole()
if check_has_stopped():
putt_controller.can_putt = true
if is_multiplayer_authority():
if not in_hole:
do_movement(delta)
rpc_sync_global_position.rpc(global_position)
apply_friction(delta)
if check_has_entered_hole():
rpc_sync_enter_hole.rpc()
if check_has_stopped():
putt_controller.can_putt = true

View file

@ -20,10 +20,16 @@ radius = 4.0
[sub_resource type="CircleShape2D" id="CircleShape2D_aigrf"]
radius = 1.0
[node name="GolfBall" type="CharacterBody2D"]
[node name="GolfBall" type="CharacterBody2D" node_paths=PackedStringArray("putt_controller", "hole_controller", "hole_sound", "player_label")]
collision_layer = 2
collision_mask = 4
motion_mode = 1
wall_min_slide_angle = 0.0
script = ExtResource("1_1uswk")
putt_controller = NodePath("PuttController")
hole_controller = NodePath("HoleController")
hole_sound = NodePath("HoleSound")
player_label = NodePath("PlayerLabel")
physics_friction = 60.0
physics_max_bounces = 4.0
physics_bounce_coefficient = 0.65

View file

@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://cywer226gp6vt"]
[sub_resource type="GDScript" id="GDScript_ek5xt"]
script/source = "extends Node
class_name GolfBallDirectory
"
[node name="GolfBallDirectory" type="Node"]
script = SubResource("GDScript_ek5xt")

View file

@ -1,6 +1,11 @@
extends Node2D
class_name GolfLevel
## Emitted when it's time to change to the next level.
signal level_completed
@export_category("Level Data")
## Whether the [field camera] follows the active player or not.
@ -23,7 +28,7 @@ class_name GolfLevel
@export var map: TileMap = null
## If on server, this variable indicates the [GolfLevel] to replicate on clients.
## If on server, this variable indicates the [GolfLevel] to replicate.
##
## The [GolfLevel] in question should not be added to the scene tree, or it will cause problems on the client acting as server.
##
@ -31,22 +36,22 @@ class_name GolfLevel
var target: GolfLevel = null
## The [PlayerNodeDirectory] to use to determine which players to spawn on clients.
## The [PlayerNodeDirectory] to use to determine which players to spawn.
##
## It should be set on instantiation of a new [GolfLevel].
var player_dir: PlayerNodeDirectory = null
## The [PackedScene] to initialize on clients as [TileMap].
## The [PackedScene] to initialize as [TileMap].
const tilemap_scene: PackedScene = preload("res://scenes/golf_tilemap.tscn")
## The [PackedScene] to initialize on clients as [GolfTee].
## The [PackedScene] to initialize as [GolfTee].
const tee_scene: PackedScene = preload("res://scenes/golf_tee.tscn")
## The [PackedScene] to initialize on clients as [GolfHole].
## The [PackedScene] to initialize as [GolfHole].
const hole_scene: PackedScene = preload("res://scenes/golf_hole.tscn")
## The [PackedScene] to initialize on clients as [Camera2D].
## The [PackedScene] to initialize as [Camera2D].
const camera_scene: PackedScene = preload("res://scenes/follow_camera.tscn")
@ -55,6 +60,7 @@ func build() -> void:
build_tilemap()
build_tilemap_cells()
build_tee()
build_balls()
build_hole()
build_camera()
@ -65,8 +71,8 @@ func build_tilemap() -> void:
var tmap: TileMap = target.map
rpc_build_tilemap.rpc(tmap.global_position, tmap.global_rotation, tmap.global_scale)
## Create the [field map] on clients.
@rpc("authority", "call_remote", "reliable")
## Create the [field map].
@rpc("authority", "call_local", "reliable")
func rpc_build_tilemap(tposition: Vector2, trotation: float, tscale: Vector2):
Log.peer(self, "Building map...")
map = tilemap_scene.instantiate()
@ -77,7 +83,7 @@ func rpc_build_tilemap(tposition: Vector2, trotation: float, tscale: Vector2):
## Replicate the cells of [field target]'s [field map] to the remote [field map].
##
## The [field map] must be already created on clients.
## The [field map] must be already created.
##
## Only takes layer 0 into consideration.
func build_tilemap_cells() -> void:
@ -90,8 +96,8 @@ func build_tilemap_cells() -> void:
var alternative_tile: int = tmap.get_cell_alternative_tile(0, coords)
rpc_build_tilemap_cell.rpc(layer, coords, source_id, atlas_coords, alternative_tile)
## Create a cell of [field map] on clients.
@rpc("authority", "call_remote", "reliable")
## Create a cell of [field map].
@rpc("authority", "call_local", "reliable")
func rpc_build_tilemap_cell(
layer: int,
coords: Vector2i,
@ -108,31 +114,29 @@ func build_tee() -> void:
Log.peer(self, "Replicating tee...")
var ttee: GolfTee = target.tee
rpc_build_tee.rpc(ttee.global_position)
rpc_build_tee_balls.rpc()
## Create the [GolfTee] object on clients.
@rpc("authority", "call_remote", "reliable")
## Create the [GolfTee] object.
@rpc("authority", "call_local", "reliable")
func rpc_build_tee(tposition: Vector2):
Log.peer(self, "Building tee...")
tee = tee_scene.instantiate()
tee.global_position = tposition
tee.everyone_entered_hole.connect(_on_everyone_entered_hole)
add_child(tee)
## Create and initialize the [GolfBall] object on clients, at the [GolfTee]'s position.
@rpc("authority", "call_remote", "reliable")
func rpc_build_tee_balls():
Log.peer(self, "Building tee balls...")
## Replicate the currently connected players' [GolfBall]s to the remote [field tee].
func build_balls() -> void:
for playernode in player_dir.get_children():
var ball: GolfBall = tee.create(playernode)
add_child(ball)
rpc_build_ball.rpc(playernode.player_name)
## Create and initialize a [GolfBall] for a single [PlayerNode] with the given name.
@rpc("authority", "call_remote", "reliable")
func rpc_build_tee_ball(player_name: String):
@rpc("authority", "call_local", "reliable")
func rpc_build_ball(player_name: String):
Log.peer(self, "Building tee ball for: %s" % player_name)
var playernode: PlayerNode = player_dir.get_playernode(player_name)
var ball: GolfBall = tee.create(playernode)
add_child(ball)
tee.spawn(playernode)
## Replicate the [field hole] of the [field target] to the remote [field hole].
@ -141,8 +145,8 @@ func build_hole() -> void:
var thole: GolfHole = target.hole
rpc_build_hole.rpc(thole.global_position, thole.global_scale)
## Create the [GolfHole] object on clients.
@rpc("authority", "call_remote", "reliable")
## Create the [GolfHole] object.
@rpc("authority", "call_local", "reliable")
func rpc_build_hole(tposition: Vector2, tscale: Vector2):
Log.peer(self, "Building hole...")
hole = hole_scene.instantiate()
@ -157,18 +161,44 @@ func build_camera() -> void:
var tcamera: FollowCamera = target.camera
rpc_build_camera.rpc(tcamera.global_position, target.camera_follows_player)
## Create the [Camera2D] object on clients.
@rpc("authority", "call_remote", "reliable")
## Create the [Camera2D] object.
@rpc("authority", "call_local", "reliable")
func rpc_build_camera(tposition: Vector2, tfocus: bool):
if multiplayer.is_server():
Log.peer(self, "Not building camera on the server.")
return
Log.peer(self, "Building camera...")
camera = camera_scene.instantiate()
camera.global_position = tposition
if tfocus:
camera.target = null # TODO: Find local player
# This supports only a single player per peer, for now.
for child in player_dir.get_children():
var playernode = child as PlayerNode
if playernode.is_multiplayer_authority():
var ctarget = tee.get_node("GolfBall__%s" % child.player_name)
camera.target = ctarget
add_child(camera)
func _on_playernode_created(node: Node):
func _ready() -> void:
player_dir.child_entered_tree.connect(_on_playernode_created)
if multiplayer.is_server():
set_physics_process(false)
hide()
func _on_playernode_created(node: Node) -> void:
Log.peer(self, "Spawning new player...")
var playernode: PlayerNode = node as PlayerNode
rpc_build_tee_ball.rpc(playernode.player_name)
rpc_build_ball.rpc(playernode.player_name)
func _on_everyone_entered_hole() -> void:
level_completed.emit()
func _on_tree_exiting() -> void:
player_dir.child_entered_tree.disconnect(_on_playernode_created)
if target:
target.queue_free()
target = null

View file

@ -4,3 +4,6 @@
[node name="GolfLevel" type="Node2D"]
script = ExtResource("1_evup0")
camera_follows_player = null
[connection signal="tree_exiting" from="." to="." method="_on_tree_exiting"]

View file

@ -8,6 +8,7 @@
[node name="GolfLevel" type="Node2D" node_paths=PackedStringArray("camera", "tee", "hole", "map")]
script = ExtResource("1_qljh7")
camera_follows_player = null
camera = NodePath("FollowCamera")
tee = NodePath("GolfTee")
hole = NodePath("GolfHole")

View file

@ -14,11 +14,10 @@ tee = NodePath("GolfTee")
hole = NodePath("GolfHole")
map = NodePath("TileMap")
[node name="FollowCamera" parent="." node_paths=PackedStringArray("target") instance=ExtResource("1_3a7ly")]
[node name="FollowCamera" parent="." instance=ExtResource("1_3a7ly")]
process_callback = 0
position_smoothing_enabled = true
drag_vertical_enabled = true
target = NodePath("")
[node name="GolfTee" parent="." instance=ExtResource("3_6q2yk")]
position = Vector2(0, -112)

View file

@ -1,22 +1,25 @@
extends Node2D
class_name GolfTee
## Emitted when all connected [GolfBall]s have their [field GolfBall.in_hole] parameter set to true.
signal everyone_entered_hole
## The [GolfBall] [PackedScene] to [method spawn].
const ball_scene: PackedScene = preload("res://scenes/golf_ball.tscn")
## Create a new [GolfBall] from [field ball_scene], initialize it with details from the given [PlayerNode], and return it.
##
## Note that this does not add the [GolfBall] to the scene tree, as it is the [GolfLevel]'s responsibility to do so.
func create(playernode: PlayerNode) -> GolfBall:
## Add a new [GolfBall] from [field ball_scene], initialize it with details from the given [PlayerNode], and return it.
func spawn(playernode: PlayerNode) -> GolfBall:
# Create the [GolfBall]
var obj: GolfBall = ball_scene.instantiate()
# Setup the initial values
obj.global_position = global_position
obj.position = Vector2.ZERO
obj.name = "GolfBall__%s" % playernode.player_name
obj.player_name = playernode.player_name
obj.player_color = playernode.player_color
obj.set_multiplayer_authority(playernode.get_multiplayer_authority())
obj.putt_controller.can_putt = is_multiplayer_authority()
# Create callables to be able to cleanup signals on destruction
var on_name_changed: Callable = _on_name_changed.bind(obj)
var on_color_changed: Callable = _on_color_changed.bind(obj)
@ -27,11 +30,27 @@ func create(playernode: PlayerNode) -> GolfBall:
playernode.color_changed.connect(on_color_changed)
playernode.possessed.connect(on_possessed)
obj.tree_exiting.connect(on_cleanup)
obj.entered_hole.connect(_on_entered_hole)
# Add the golf ball as a child of the tee
add_child(obj)
# Return the created [GolfBall]
return obj
func is_everyone_in_hole() -> bool:
for child in get_children():
var ball: GolfBall = child as GolfBall
if ball == null:
# Ignore collision sounds
continue
if ball.get_multiplayer_authority() == 1:
continue
if not ball.in_hole:
return false
return true
func _on_name_changed(_old: String, new: String, obj: GolfBall) -> void:
obj.name = "GolfBall__%s" % new
obj.player_name = new
func _on_color_changed(_old: Color, new: Color, obj: GolfBall) -> void:
@ -39,8 +58,13 @@ func _on_color_changed(_old: Color, new: Color, obj: GolfBall) -> void:
func _on_possessed(_old: int, new: int, obj: GolfBall) -> void:
obj.set_multiplayer_authority(new)
obj.putt_controller.can_putt = is_multiplayer_authority()
func _on_cleanup(playernode: PlayerNode, on_name_changed: Callable, on_color_changed: Callable, on_possessed: Callable) -> void:
playernode.name_changed.disconnect(on_name_changed)
playernode.color_changed.disconnect(on_color_changed)
playernode.possessed.disconnect(on_possessed)
func _on_entered_hole(_strokes: int) -> void:
if is_everyone_in_hole():
everyone_entered_hole.emit()

View file

@ -20,20 +20,23 @@ var level: GolfLevel = null
func rpc_next_level():
# Destroy the current level
if level != null:
level.target.queue_free()
player_dir.child_entered_tree.disconnect(level._on_playernode_created)
level.queue_free()
level = null
# Create the new level
level = base_scene.instantiate()
level.player_dir = player_dir
level.level_completed.connect(_on_level_completed)
# Determine the next level
if multiplayer.is_server():
var target = playlist.advance().instantiate()
level.target = target
player_dir.child_entered_tree.connect(level._on_playernode_created)
# Add the level to the tree
add_child(level)
add_child(level, true)
# Start the replication
if multiplayer.is_server():
level.build()
func _on_level_completed() -> void:
if multiplayer.is_server():
rpc_next_level.rpc()

View file

@ -87,7 +87,16 @@ func init_lobby_menu() -> void:
interface_instance.add_child(lobby_menu_instance)
func deinit_lobby_menu() -> void:
# TODO: Disconnect all signals above
lobby_menu_instance.leave_confirmed.disconnect(_on_leave_confirmed)
if server_game_instance:
lobby_menu_instance.start_confirmed.disconnect(server_game_instance._on_main_start_confirmed)
client_game_instance.multiplayer.server_disconnected.disconnect(_on_leave_confirmed)
client_game_instance.player_dir.child_entered_tree.disconnect(lobby_menu_instance.players_list._on_playernode_created)
client_game_instance.player_dir.child_exiting_tree.disconnect(lobby_menu_instance.players_list._on_playernode_destroyed)
client_game_instance.player_dir.playernode_name_changed.disconnect(lobby_menu_instance.players_list._on_playernode_name_changed)
client_game_instance.player_dir.playernode_color_changed.disconnect(lobby_menu_instance.players_list._on_playernode_color_changed)
client_game_instance.player_dir.playernode_possessed.disconnect(lobby_menu_instance.players_list._on_playernode_possessed)
client_game_instance.phase_tracker.phase_changed.disconnect(_on_phase_changed)
lobby_menu_instance.queue_free()
func _ready() -> void:
@ -105,8 +114,10 @@ func _on_connecting_confirmed(player_name: String, player_color: Color, server_a
init_lobby_menu()
func _on_leave_confirmed() -> void:
deinit_lobby_menu()
deinit_client_game()
if lobby_menu_scene:
deinit_lobby_menu()
if client_game_instance:
deinit_client_game()
if server_game_instance != null:
deinit_server_game()
init_main_menu()

View file

@ -1,24 +1,54 @@
extends Node2D
class_name PuttController
@export_category("References")
## The [Sprite2D] used to calculate [field sprite_texture_width] from.
@export var sprite: Sprite2D
## The [AudioStreamPlayer2D] to play when a putt happens.
@export var sound: AudioStreamPlayer2D
@export_category("Physics")
## The maximum impulse that a putt can have.
@export var putt_max_impulse: float
@export_category("Scale")
## How many game units a pixel of screen mouse movement corresponds to.
@export var putt_drag_scale: float
## Length multiplier of the putt ghost
@export var putt_ghost_scale: float
## Curve mapping relative putt impulse to putt sound volume.
@export var putt_volume: Curve
## Emitted when a putt has happened.
signal putt(putt_vector: Vector2)
@onready var sprite: Sprite2D = $Sprite
@onready var sound: AudioStreamPlayer2D = $Sound
## The width in pixels that the putt ghost should have.
@onready var sprite_texture_width: float = sprite.texture.get_width()
## Whether a putt is currently in progress of not.
##
## If this is true, then [field drag_start_point] should contain a value.
var is_putting: bool = false
var putt_start_point: Vector2
## The position on the screen where the putt has started.
var drag_start_point: Vector2
## Whether a putt can currently be performed or not.
var can_putt: bool = false:
get:
return can_putt
@ -30,7 +60,7 @@ var can_putt: bool = false:
func _input(event: InputEvent):
if can_putt:
if event is InputEventMouseButton:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
if not is_putting:
start_putt(event.position)
@ -42,10 +72,10 @@ func _input(event: InputEvent):
else:
push_warning("Attempted to end putt while none was in progress.")
if is_putting:
update_putt_ghost(compute_putt(event.position, putt_start_point))
update_putt_ghost(compute_putt(event.position, drag_start_point))
if event is InputEventMouseMotion:
if is_putting:
update_putt_ghost(compute_putt(event.position, putt_start_point))
update_putt_ghost(compute_putt(event.position, drag_start_point))
func update_putt_ghost(putt_vector: Vector2):
@ -65,14 +95,14 @@ func compute_putt(start_position: Vector2, end_position: Vector2) -> Vector2:
func start_putt(start_position: Vector2):
visible = true
is_putting = true
putt_start_point = start_position
drag_start_point = start_position
func end_putt(end_position: Vector2):
visible = false
is_putting = false
can_putt = false
var putt_vector = compute_putt(putt_start_point, end_position)
var putt_vector = compute_putt(drag_start_point, end_position)
putt.emit(putt_vector)
play_putt_sound(putt_vector)

View file

@ -10,10 +10,12 @@ max_value = 0.0
_data = [Vector2(0, -16), 0.0, 16.0, 0, 1, Vector2(1, 0), 16.0, 0.0, 1, 0]
point_count = 2
[node name="PuttController" type="Node2D"]
[node name="PuttController" type="Node2D" node_paths=PackedStringArray("sprite", "sound")]
visible = false
modulate = Color(1, 1, 1, 0.25098)
script = ExtResource("1_rb6v1")
sprite = NodePath("Sprite")
sound = NodePath("Sound")
putt_max_impulse = 333.0
putt_drag_scale = 1.0
putt_ghost_scale = 0.3

View file

@ -31,7 +31,8 @@ use_texture_padding = false
[resource]
tile_size = Vector2i(128, 128)
physics_layer_0/collision_layer = 1
physics_layer_0/collision_layer = 4
physics_layer_0/collision_mask = 0
sources/1 = SubResource("TileSetAtlasSource_174yq")
sources/2 = SubResource("TileSetAtlasSource_2md10")
sources/3 = SubResource("TileSetAtlasSource_mbu12")