Player bounds checking, better scaling calc on client
This commit is contained in:
parent
07f02d5a32
commit
901c4b773b
11 changed files with 209 additions and 118 deletions
29
client/danmaku!/Board.tscn
Normal file
29
client/danmaku!/Board.tscn
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://b1m2pclbncn68"]
|
||||
|
||||
[ext_resource type="Script" path="res://danmaku!/scaling.gd" id="1_mxxs1"]
|
||||
[ext_resource type="PackedScene" uid="uid://cd3tqt7hr5pqs" path="res://danmaku!/player.tscn" id="2_d6n3g"]
|
||||
[ext_resource type="Script" path="res://danmaku!/network_manager.gd" id="3_4fnyw"]
|
||||
|
||||
[node name="Board" type="AspectRatioContainer"]
|
||||
anchors_preset = -1
|
||||
anchor_right = 0.263889
|
||||
anchor_bottom = 0.257716
|
||||
offset_right = 14.0
|
||||
offset_bottom = 27.0
|
||||
ratio = 0.5625
|
||||
script = ExtResource("1_mxxs1")
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="ReferenceRect" type="ReferenceRect" parent="."]
|
||||
layout_mode = 2
|
||||
border_color = Color(0.995542, 0.845388, 0.421763, 1)
|
||||
editor_only = false
|
||||
|
||||
[node name="Container" type="Node2D" parent="ReferenceRect"]
|
||||
scale = Vector2(1.2125, 1.2125)
|
||||
|
||||
[node name="Player" parent="ReferenceRect/Container" instance=ExtResource("2_d6n3g")]
|
||||
|
||||
[node name="NetworkManager" type="Node2D" parent="ReferenceRect/Container" node_paths=PackedStringArray("player")]
|
||||
script = ExtResource("3_4fnyw")
|
||||
player = NodePath("../Player")
|
||||
21
client/danmaku!/Game.tscn
Normal file
21
client/danmaku!/Game.tscn
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://3a8txh83qfu5"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://b1m2pclbncn68" path="res://danmaku!/Board.tscn" id="1_pv3ov"]
|
||||
|
||||
[node name="Game" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Board" parent="." instance=ExtResource("1_pv3ov")]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_right = 0.0
|
||||
offset_bottom = 0.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
2
client/danmaku!/globals.gd
Normal file
2
client/danmaku!/globals.gd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
class_name Globals
|
||||
const SERVER_SIZE = Vector2(90.0, 160.0)
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
extends Node
|
||||
extends Node2D
|
||||
var nakama_client: NakamaClient
|
||||
var nakama_session: NakamaSession
|
||||
var nakama_socket: NakamaSocket
|
||||
|
||||
const SERVER_WIDTH = 90.0
|
||||
const SERVER_HEIGHT = 160.0
|
||||
@export var player: Player
|
||||
|
||||
var predicted_tick = 0
|
||||
var delta_counter = 0
|
||||
|
|
@ -18,7 +17,7 @@ func _ready() -> void:
|
|||
print("Attempting to create debug match.")
|
||||
await create_and_join_debug_match()
|
||||
nakama_socket.received_match_state.connect(self._on_match_state)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if current_match_id == "":
|
||||
return
|
||||
|
|
@ -30,7 +29,7 @@ func _process(delta: float) -> void:
|
|||
var next_pos = bullet.get_current_pos(predicted_tick + 1)
|
||||
var interpolated_pos = prev_pos.lerp(next_pos, bullet_lerp_factor)
|
||||
|
||||
bullet.position = world_to_screen(interpolated_pos)
|
||||
bullet.position = interpolated_pos
|
||||
|
||||
#var screen_size = get_viewport().size
|
||||
#bullets = bullets.filter(func(bullet):
|
||||
|
|
@ -40,22 +39,28 @@ func _process(delta: float) -> void:
|
|||
|
||||
func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
||||
match p_state.op_code:
|
||||
2: # Server state update
|
||||
2:
|
||||
var data = JSON.parse_string(p_state.data)
|
||||
var bullet = DanmakuBullet.new()
|
||||
bullet.setup_bullet(
|
||||
int(data["class"]),
|
||||
int(data["tick"]),
|
||||
data["x"],
|
||||
data["y"],
|
||||
data["vel_x"],
|
||||
data["vel_y"])
|
||||
bullet.position = world_to_screen(bullet.get_current_pos(int(data["tick"])))
|
||||
bullet.texture = load("res://test-bullet.png")
|
||||
add_child(bullet)
|
||||
bullets.append(bullet)
|
||||
#delta_counter = 0
|
||||
#predicted_tick = int(data["tick"])
|
||||
# Set player position given server bounds-checking
|
||||
if data["forcePlayerPos"]:
|
||||
player.position = Vector2(float(data["playerPos"]["x"]), float(data["playerPos"]["y"]))
|
||||
|
||||
# Spawn new bullets
|
||||
for b in data["newBullets"]:
|
||||
var bullet = DanmakuBullet.new()
|
||||
bullet.setup_bullet(
|
||||
int(b["class"]),
|
||||
int(b["tick"]),
|
||||
b["x"],
|
||||
b["y"],
|
||||
b["vel_x"],
|
||||
b["vel_y"])
|
||||
bullet.texture = load("res://test-bullet.png")
|
||||
bullet.position = bullet.get_current_pos(int(b["tick"]))
|
||||
bullet.scale = Vector2(0.2, 0.2)
|
||||
add_child(bullet)
|
||||
bullets.append(bullet)
|
||||
#predicted_tick = int(b["tick"])
|
||||
|
||||
func attempt_auth() -> void:
|
||||
nakama_client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http")
|
||||
|
|
@ -83,34 +88,14 @@ func create_and_join_debug_match() -> void:
|
|||
else:
|
||||
current_match_id = response.payload
|
||||
|
||||
func world_to_screen(server_pos: Vector2) -> Vector2:
|
||||
var screen_size = get_viewport().size
|
||||
var scale_x = screen_size.x / SERVER_WIDTH
|
||||
var scale_y = screen_size.y / SERVER_HEIGHT
|
||||
|
||||
var client_x = server_pos.x * scale_x
|
||||
var client_y = server_pos.y * scale_y
|
||||
|
||||
return Vector2(client_x, client_y)
|
||||
|
||||
func screen_to_world(client_pos: Vector2) -> Vector2:
|
||||
var screen_size = get_viewport().size
|
||||
var scale_x = SERVER_WIDTH / screen_size.x
|
||||
var scale_y = SERVER_HEIGHT / screen_size.y
|
||||
|
||||
var server_x = client_pos.x * scale_x
|
||||
var server_y = client_pos.y * scale_y
|
||||
|
||||
return Vector2(server_x, server_y)
|
||||
|
||||
func predict_tick_and_broadcast(delta):
|
||||
delta_counter += delta
|
||||
|
||||
# New tick, broadcast player inputs
|
||||
if delta_counter >= 0.05:
|
||||
predicted_tick += 1
|
||||
delta_counter = 0
|
||||
var position = screen_to_world(get_node("../Player").position)
|
||||
var json_string = JSON.stringify({"x": position.x, "y": position.y})
|
||||
delta_counter -= 0.05
|
||||
var pos = get_node("../Player").position
|
||||
var json_string = JSON.stringify({"x": pos.x, "y": pos.y})
|
||||
nakama_socket.send_match_state_async(current_match_id, 0, json_string)
|
||||
bullet_lerp_factor = delta_counter / 0.05
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
class_name Player
|
||||
extends Node2D
|
||||
|
||||
@export var speed = 400
|
||||
var velocity = 0
|
||||
@export var speed = 80
|
||||
var velocity := Vector2.ZERO
|
||||
|
||||
func get_input():
|
||||
if Input.is_action_pressed("Slow Mode"):
|
||||
speed = 200
|
||||
speed = 40
|
||||
else:
|
||||
speed = 400
|
||||
speed = 80
|
||||
|
||||
var input_direction = Input.get_vector("Left", "Right", "Up", "Down")
|
||||
velocity = input_direction * speed
|
||||
velocity = Input.get_vector("Left", "Right", "Up", "Down") * speed
|
||||
|
||||
func _physics_process(delta):
|
||||
func _physics_process(delta: float):
|
||||
get_input()
|
||||
position += velocity * delta
|
||||
|
||||
# Bounds checking
|
||||
var attempted_position := position + (velocity * delta)
|
||||
attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
|
||||
|
||||
position = attempted_position
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@
|
|||
script = ExtResource("1_l6typ")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
scale = Vector2(0.09, 0.09)
|
||||
texture = ExtResource("2_j7sx3")
|
||||
|
|
|
|||
6
client/danmaku!/scaling.gd
Normal file
6
client/danmaku!/scaling.gd
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
@tool
|
||||
extends AspectRatioContainer
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var board_screen_size = $ReferenceRect.get_rect()
|
||||
$ReferenceRect/Container.scale = Vector2(board_screen_size.size.x / Globals.SERVER_SIZE.x, board_screen_size.size.y / Globals.SERVER_SIZE.y)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
[gd_scene load_steps=3 format=3 uid="uid://b1m2pclbncn68"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://cd3tqt7hr5pqs" path="res://danmaku!/player.tscn" id="1_jeq34"]
|
||||
[ext_resource type="Script" path="res://danmaku!/network_manager.gd" id="2_3453q"]
|
||||
|
||||
[node name="Testworld" type="Node2D"]
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("1_jeq34")]
|
||||
|
||||
[node name="NetworkManager" type="Node" parent="."]
|
||||
script = ExtResource("2_3453q")
|
||||
|
|
@ -11,7 +11,7 @@ config_version=5
|
|||
[application]
|
||||
|
||||
config/name="danmaku!"
|
||||
run/main_scene="res://danmaku!/testworld.tscn"
|
||||
run/main_scene="res://danmaku!/Game.tscn"
|
||||
config/features=PackedStringArray("4.3", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
package main
|
||||
|
||||
const (
|
||||
BULLET_LINEAR = 0
|
||||
)
|
||||
|
||||
const (
|
||||
STAGE_WIDTH float64 = 90.0
|
||||
STAGE_HEIGHT float64 = 160.0
|
||||
BULLET_KILL_BUFFER_WIDTH float64 = 16.0
|
||||
)
|
||||
152
server/main.go
152
server/main.go
|
|
@ -12,6 +12,7 @@ import (
|
|||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"math/rand"
|
||||
"slices"
|
||||
|
||||
|
|
@ -26,26 +27,42 @@ const (
|
|||
MATCH_END
|
||||
)
|
||||
|
||||
const (
|
||||
BULLET_LINEAR = 0
|
||||
)
|
||||
|
||||
const (
|
||||
STAGE_WIDTH float64 = 90.0
|
||||
STAGE_HEIGHT float64 = 160.0
|
||||
BULLET_KILL_BUFFER_WIDTH float64 = 16.0
|
||||
)
|
||||
|
||||
// Interface for registering match handlers
|
||||
type BattleRoyaleMatch struct{}
|
||||
|
||||
type PlayerStageState struct {
|
||||
xPos float64
|
||||
yPos float64
|
||||
bullets []*C.Bullet
|
||||
}
|
||||
|
||||
type PlayerMessageData struct {
|
||||
type Position struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
}
|
||||
|
||||
type PlayerStageState struct {
|
||||
pos Position
|
||||
bullets []*C.Bullet
|
||||
updatePlayerPos bool
|
||||
}
|
||||
|
||||
type GameTickUpdate struct {
|
||||
Tick int64 `json:"tick"`
|
||||
PlayerPos Position `json:"playerPos"`
|
||||
NewBullets []map[string]interface{} `json:"newBullets"`
|
||||
ForcePlayerPos bool `json:"forcePlayerPos"`
|
||||
}
|
||||
|
||||
type PresenceState struct { // present time! hahahahahahahah!
|
||||
presence runtime.Presence
|
||||
stageState PlayerStageState
|
||||
}
|
||||
|
||||
// In-memory game state
|
||||
type BattleRoyaleMatchState struct {
|
||||
tickRate int
|
||||
currentMatchPhase int
|
||||
|
|
@ -92,9 +109,12 @@ func (m *BattleRoyaleMatch) MatchJoin(ctx context.Context, logger runtime.Logger
|
|||
lobbyState.presences[presences[i].GetSessionId()] = &PresenceState{
|
||||
presence: presences[i],
|
||||
stageState: PlayerStageState{
|
||||
xPos: STAGE_WIDTH * 0.5,
|
||||
yPos: STAGE_HEIGHT - STAGE_HEIGHT*0.1,
|
||||
bullets: []*C.Bullet{},
|
||||
pos: Position{
|
||||
X: STAGE_WIDTH * 0.5,
|
||||
Y: STAGE_HEIGHT - STAGE_HEIGHT*0.1,
|
||||
},
|
||||
bullets: []*C.Bullet{},
|
||||
updatePlayerPos: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +131,16 @@ func (m *BattleRoyaleMatch) MatchLeave(ctx context.Context, logger runtime.Logge
|
|||
}
|
||||
|
||||
for i := 0; i < len(presences); i++ {
|
||||
delete(lobbyState.presences, presences[i].GetSessionId())
|
||||
sessionID := presences[i].GetSessionId()
|
||||
|
||||
playerState, exists := lobbyState.presences[sessionID]
|
||||
if exists {
|
||||
for _, bullet := range playerState.stageState.bullets {
|
||||
C.destroy_bullet(bullet)
|
||||
}
|
||||
|
||||
delete(lobbyState.presences, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
return lobbyState
|
||||
|
|
@ -158,9 +187,52 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
|||
return nil
|
||||
}
|
||||
|
||||
// Test bullet spawning
|
||||
if tick%1 == 0 {
|
||||
for _, v := range lobbyState.presences {
|
||||
// Respond to player input
|
||||
for _, msg := range messages {
|
||||
// Validate player existence
|
||||
_, exists := lobbyState.presences[msg.GetSessionId()]
|
||||
if !exists {
|
||||
logger.Warn("Received input for non-existent player session ID: %v", msg.GetSessionId())
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse player message
|
||||
var pos Position
|
||||
if err := json.Unmarshal(msg.GetData(), &pos); err != nil {
|
||||
logger.Warn("Failed to parse input: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Player movement bounds detection
|
||||
clampedX := pos.X < 0 || pos.X > STAGE_WIDTH
|
||||
clampedY := pos.Y < 0 || pos.Y > STAGE_HEIGHT
|
||||
|
||||
pos.X = math.Max(0, math.Min(pos.X, STAGE_WIDTH))
|
||||
pos.Y = math.Max(0, math.Min(pos.Y, STAGE_HEIGHT))
|
||||
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.pos.X = pos.X
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.pos.Y = pos.Y
|
||||
|
||||
if clampedX || clampedY {
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.updatePlayerPos = true
|
||||
}
|
||||
}
|
||||
|
||||
// Compute and broadcast per-presence state
|
||||
for _, v := range lobbyState.presences {
|
||||
// Clean up bullets when they pass off the board
|
||||
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
|
||||
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
|
||||
C.destroy_bullet(b)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
var newBulletsToBroadcast = []map[string]interface{}{}
|
||||
|
||||
// Test bullet spawning
|
||||
if tick%1 == 0 {
|
||||
velx := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
|
||||
vely := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
|
||||
vel_x_sign := 2*rand.Intn(2) - 1
|
||||
|
|
@ -175,6 +247,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
|||
C.double(float64(vel_y_sign)*vely),
|
||||
)
|
||||
|
||||
v.stageState.bullets = append(v.stageState.bullets, bullet)
|
||||
|
||||
var x, y C.double
|
||||
C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y)
|
||||
|
||||
|
|
@ -187,38 +261,28 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
|
|||
"vel_y": float64(vel_y_sign) * vely,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(bulletData)
|
||||
if err != nil {
|
||||
logger.Error("Error marshalling bullet data", err)
|
||||
} else {
|
||||
v.stageState.bullets = append(v.stageState.bullets, bullet)
|
||||
reliable := true
|
||||
dispatcher.BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Respond to player input
|
||||
for _, msg := range messages {
|
||||
var pos PlayerMessageData
|
||||
if err := json.Unmarshal(msg.GetData(), &pos); err != nil {
|
||||
logger.Warn("Failed to parse input: %v", err)
|
||||
continue
|
||||
newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData)
|
||||
}
|
||||
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.xPos = pos.X
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.yPos = pos.Y
|
||||
}
|
||||
var tickData = GameTickUpdate{
|
||||
Tick: tick,
|
||||
PlayerPos: Position{
|
||||
X: v.stageState.pos.X,
|
||||
Y: v.stageState.pos.Y,
|
||||
},
|
||||
NewBullets: newBulletsToBroadcast,
|
||||
ForcePlayerPos: v.stageState.updatePlayerPos,
|
||||
}
|
||||
|
||||
// Bullet cleanup when off board
|
||||
for _, v := range lobbyState.presences {
|
||||
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
|
||||
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
|
||||
C.destroy_bullet(b)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
v.stageState.updatePlayerPos = false
|
||||
|
||||
data, err := json.Marshal(tickData)
|
||||
if err != nil {
|
||||
logger.Error("Error marshalling bullet data", err)
|
||||
} else {
|
||||
reliable := true
|
||||
dispatcher.BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable)
|
||||
}
|
||||
}
|
||||
|
||||
return lobbyState
|
||||
|
|
|
|||
Reference in a new issue