Improved reconnect flow

This commit is contained in:
Eduard Urbach 2024-01-29 23:16:06 +01:00
parent bf08205c7d
commit b22d0b1367
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
25 changed files with 212 additions and 99 deletions

View File

@ -1,26 +1,40 @@
extends Node
func _ready():
pause(true)
Global.instance_id = OS.get_process_id() % 4
Global.username = "user%d" % Global.instance_id
%Login.success.connect(on_login)
%Logout.success.connect(on_logout)
%Login.send_login()
mute_audio()
func _input(event):
if event.is_action_pressed("toggle_fullscreen"):
toggle_fullscreen()
get_viewport().set_input_as_handled()
func mute_audio():
var master_sound = AudioServer.get_bus_index("Master")
AudioServer.set_bus_mute(master_sound, true)
func on_login():
print("[%s] Login succeeded." % Global.username)
print("[%s] ID: %s" % [Global.username, Global.account_id])
print("[%s] Auth token: %s" % [Global.username, Global.auth_token])
DisplayServer.window_set_title("%s - %s" % [Global.username, Global.account_id])
DisplayServer.window_set_position(Vector2((Global.instance_id % 2) * 960, (Global.instance_id / 2 % 2) * 540))
pause(false)
func on_logout():
print("[%s] Logout." % Global.username)
pause(true)
func pause(enabled: bool):
get_tree().paused = enabled
mute_audio(enabled)
func mute_audio(enabled: bool):
var master_sound = AudioServer.get_bus_index("Master")
AudioServer.set_bus_mute(master_sound, enabled)
func toggle_fullscreen():
var mode = DisplayServer.window_get_mode()

View File

@ -1,10 +1,11 @@
[gd_scene load_steps=24 format=3 uid="uid://b40y7iuskv1ar"]
[gd_scene load_steps=25 format=3 uid="uid://b40y7iuskv1ar"]
[ext_resource type="Script" path="res://Main.gd" id="1_cw3ws"]
[ext_resource type="Script" path="res://network/Client.gd" id="2_8hxcx"]
[ext_resource type="Script" path="res://network/Ping.gd" id="3_d6qf1"]
[ext_resource type="Script" path="res://network/Login.gd" id="4_fsx7a"]
[ext_resource type="Script" path="res://network/PlayerAdd.gd" id="5_376ik"]
[ext_resource type="Script" path="res://network/Logout.gd" id="5_au5w3"]
[ext_resource type="PackedScene" uid="uid://2lcnu3dy54lx" path="res://player/Player.tscn" id="6_cdj8w"]
[ext_resource type="Script" path="res://network/PlayerRemove.gd" id="7_2r42o"]
[ext_resource type="Script" path="res://network/PlayerMove.gd" id="8_ke1yy"]
@ -40,6 +41,7 @@ script = ExtResource("1_cw3ws")
[node name="Client" type="Node" parent="."]
unique_name_in_owner = true
process_mode = 3
script = ExtResource("2_8hxcx")
[node name="Ping" type="Node" parent="Client"]
@ -59,6 +61,11 @@ packet_type = 2
wait_time = 5.0
autostart = true
[node name="Logout" type="Node" parent="Client"]
unique_name_in_owner = true
script = ExtResource("5_au5w3")
packet_type = 3
[node name="PlayerAdd" type="Node" parent="Client"]
unique_name_in_owner = true
script = ExtResource("5_376ik")
@ -71,7 +78,6 @@ packet_type = 11
[node name="PlayerMove" type="Node" parent="Client"]
script = ExtResource("8_ke1yy")
delay = null
packet_type = 12
[node name="PlayerJump" type="Node" parent="Client"]
@ -131,6 +137,7 @@ unique_name_in_owner = true
script = ExtResource("16_dp6bj")
[node name="UI" parent="." instance=ExtResource("17_43qhq")]
process_mode = 3
[node name="Viewport" type="SubViewportContainer" parent="."]
texture_filter = 1

View File

@ -8,7 +8,7 @@ func send_message(message: String):
buffer.put_data(message.to_utf8_buffer())
%Client.send(buffer.data_array)
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var buffer := StreamPeerBuffer.new()
buffer.data_array = data

View File

@ -21,7 +21,7 @@ func _enter_tree():
func _process(_delta):
while socket.get_available_packet_count() > 0:
var packet := socket.get_packet()
handle_packet(packet, socket)
handle_packet(packet)
func update_statistics():
download_changed.emit(download)
@ -31,4 +31,4 @@ func update_statistics():
func send(data: PackedByteArray):
socket.put_packet(data)
upload += data.size()
upload += data.size()

View File

@ -3,7 +3,7 @@ extends PacketHandler
signal success
signal failure
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var buffer := StreamPeerBuffer.new()
buffer.data_array = data
@ -15,10 +15,6 @@ func handle_packet(data: PackedByteArray, _peer: PacketPeer):
Global.account_id = buffer.get_string()
Global.auth_token = buffer.get_string()
print("[%s] Login succeeded." % Global.username)
print("[%s] ID: %s" % [Global.username, Global.account_id])
print("[%s] Auth token: %s" % [Global.username, Global.auth_token])
success.emit()

12
client/network/Logout.gd Normal file
View File

@ -0,0 +1,12 @@
extends PacketHandler
signal success
func handle_packet(_data: PackedByteArray):
logout()
success.emit()
func logout():
Global.auth_token = ""
Global.account_id = ""
Global.player = null

View File

@ -10,7 +10,7 @@ var history: Array[float] = []
func _init():
history.resize(HISTORY_SIZE)
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var id := data[0]
var ping := get_time() - history[id]
changed.emit(ping)

View File

@ -7,7 +7,7 @@ signal main_player_spawned(player: Player)
func _ready():
assert(player_scene)
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var buffer := StreamPeerBuffer.new()
buffer.data_array = data
@ -24,19 +24,29 @@ func handle_packet(data: PackedByteArray, _peer: PacketPeer):
var player := spawn_player(player_id)
player.position = server_position
player.set_character_name(player_name)
Global.players.add(player)
func spawn_player(id: String) -> Player:
var player = player_scene.instantiate()
var player: Player
if Global.players.has(id):
player = Global.players.get_player(id)
if id == Global.account_id:
Global.player = player
return player
player = player_scene.instantiate()
player.id = id
if id == Global.account_id:
Global.player = player
player.controller = PlayerController.new()
Global.player = player
main_player_spawned.emit(player)
else:
player.controller = ProxyController.new()
player.controller.character = player
player.add_child(player.controller)
%Players.add(player)
return player

View File

@ -11,7 +11,7 @@ func on_jump():
buffer.put_8(PacketHandler.Packet.PLAYER_JUMP)
%Client.send(buffer.data_array)
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var player_id := data.get_string_from_ascii()
var player := Global.players.get_player(player_id)
player.jump()

View File

@ -5,7 +5,7 @@ extends PacketHandler
var last_sent := Time.get_ticks_msec()
var last_sent_position := Vector3.ZERO
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var buffer := StreamPeerBuffer.new()
buffer.data_array = data

View File

@ -1,6 +1,6 @@
extends PacketHandler
func handle_packet(data: PackedByteArray, _peer: PacketPeer):
func handle_packet(data: PackedByteArray):
var player_id := data.get_string_from_ascii()
print("[%s] Remove player: %s" % [Global.username, player_id])
%Players.remove(player_id)
Global.players.remove(player_id)

View File

@ -13,7 +13,7 @@ func get_handler(index: int) -> PacketHandler:
func set_handler(index: int, node: PacketHandler):
handlers[index] = node
func handle_packet(packet: PackedByteArray, peer: PacketPeer):
func handle_packet(packet: PackedByteArray):
var type := packet.decode_u8(0)
var handler := get_handler(type)
@ -21,5 +21,5 @@ func handle_packet(packet: PackedByteArray, peer: PacketPeer):
push_warning("Unknown packet type %d" % type)
return
handler.handle_packet(packet.slice(1), peer)
handler.handle_packet(packet.slice(1))
download += packet.size()

View File

@ -16,5 +16,5 @@ enum Packet {
@export var packet_type: Packet
func handle_packet(_data: PackedByteArray, _peer: PacketPeer):
func handle_packet(_data: PackedByteArray):
pass

View File

@ -0,0 +1,5 @@
class_name Controller
extends Node
## The character that we're controlling.
@export var character: Character

View File

@ -1,8 +1,5 @@
class_name PlayerController
extends Node
## The character that we're controlling.
@export var character: Character
extends Controller
var move: Vector2

View File

@ -1,20 +1,19 @@
class_name ProxyController
extends Node
## The character that we're controlling.
@export var character: Character
extends Controller
## The authoritative position on the server.
var server_position: Vector3
func _ready():
print(character.name, ".old: ", server_position)
server_position = character.position
print(character.name, ".new: ", server_position)
func _process(_delta):
var move := server_position - character.position
move.y = 0.0
if move.length_squared() < 0.01:
# character.position = server_position
character.direction = Vector3.ZERO
return

View File

@ -7,9 +7,13 @@ var ping_changed: Signal
var download_changed: Signal
var upload_changed: Signal
var message_received: Signal
var login: Signal
var logout: Signal
func _enter_tree():
ping_changed = %Ping.changed
download_changed = %Client.download_changed
upload_changed = %Client.upload_changed
message_received = %Chat.message_received
login = %Login.success
logout = %Logout.success

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=11 format=3 uid="uid://dagn5bf7ou3sd"]
[gd_scene load_steps=12 format=3 uid="uid://dagn5bf7ou3sd"]
[ext_resource type="PackedScene" uid="uid://cch67vqpsmtej" path="res://ui/debug/DebugLabel.tscn" id="1_7s8uu"]
[ext_resource type="Script" path="res://ui/UI.gd" id="1_l5b6o"]
@ -10,6 +10,7 @@
[ext_resource type="Script" path="res://ui/chat/ChatInput.gd" id="6_cg2h5"]
[ext_resource type="Script" path="res://ui/debug/UploadLabel.gd" id="7_cfnpx"]
[ext_resource type="Script" path="res://ui/debug/DownloadLabel.gd" id="8_ogt38"]
[ext_resource type="Script" path="res://ui/connect/ConnectPanel.gd" id="11_cwl0t"]
[node name="UI" type="Control"]
layout_mode = 3
@ -102,6 +103,29 @@ layout_mode = 2
alignment = 2
script = ExtResource("5_8lm6a")
[node name="ConnectPanel" type="Panel" parent="CanvasLayer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("11_cwl0t")
[node name="ConnectLabel" type="Label" parent="CanvasLayer/ConnectPanel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -47.5
offset_top = -10.0
offset_right = 47.5
offset_bottom = 10.0
grow_horizontal = 2
grow_vertical = 2
text = "Connecting..."
[connection signal="focus_entered" from="CanvasLayer/BottomLeft/Chat/ChatInput" to="CanvasLayer/BottomLeft/Chat/ChatInput" method="_on_focus_entered"]
[connection signal="focus_exited" from="CanvasLayer/BottomLeft/Chat/ChatInput" to="CanvasLayer/BottomLeft/Chat/ChatInput" method="_on_focus_exited"]
[connection signal="text_submitted" from="CanvasLayer/BottomLeft/Chat/ChatInput" to="CanvasLayer/BottomLeft/Chat/ChatInput" method="_on_text_submitted"]

View File

@ -0,0 +1,11 @@
extends Panel
func _ready():
owner.login.connect(on_login)
owner.logout.connect(on_logout)
func on_login():
hide()
func on_logout():
show()

View File

@ -7,12 +7,18 @@ func _ready():
Global.players = self
func add(player: Player):
if has(player.id):
return
add_child(player)
players[player.id] = player
func get_player(id: String) -> Player:
return players[id] as Player
func has(id: String) -> bool:
return players.has(id)
func remove(id: String):
if !players.has(id):
return

View File

@ -2,6 +2,7 @@ package game
import (
"net"
"strings"
)
// Chat is used for chat messages.
@ -12,9 +13,30 @@ func (game *Game) Chat(data []byte, address *net.UDPAddr) error {
return ErrUnknownAddress
}
message := string(data)
if game.ChatCommand(player, message) {
return nil
}
newData := []byte{}
newData = AppendString(newData, player.ID)
newData = AppendStringBytes(newData, data)
newData = AppendString(newData, message)
game.Broadcast(Chat, newData)
return nil
}
// ChatCommand executes chat commands and returns true if it was one.
func (game *Game) ChatCommand(player *Player, message string) bool {
if strings.HasPrefix(message, "/") {
switch message {
case "/logout":
game.server.Send(Logout, nil, player.address)
game.players.Remove(player)
}
return true
}
return false
}

View File

@ -10,7 +10,7 @@ import (
// Game represents the entire state of the game server.
type Game struct {
server *Server
router Router
router *Router
players *PlayerManager
}
@ -21,19 +21,26 @@ func New() *Game {
players: NewPlayerManager(),
}
game.router.Get(Ping, game.Ping)
game.router.Get(Login, game.Login)
game.router.Get(Move, game.Move)
game.router.Get(Jump, game.Jump)
game.router.Get(Chat, game.Chat)
game.router = NewRouter(game)
return game
}
// NewRouter creates a new router.
func NewRouter(game *Game) *Router {
router := &Router{}
router.Get(Ping, game.Ping)
router.Get(Login, game.Login)
router.Get(Move, game.Move)
router.Get(Jump, game.Jump)
router.Get(Chat, game.Chat)
return router
}
// Run starts all game systems.
func (game *Game) Run() {
physics := time.NewTicker(20 * time.Millisecond)
statistics := time.NewTicker(time.Second)
clean := time.NewTicker(time.Second)
physics := time.NewTicker(20 * time.Millisecond).C
statistics := time.NewTicker(time.Second).C
clean := time.NewTicker(time.Second).C
close := make(chan os.Signal, 1)
signal.Notify(close, os.Interrupt)
@ -44,16 +51,16 @@ func (game *Game) Run() {
case p := <-game.server.incoming:
game.router.handle(p)
case <-physics.C:
case <-physics:
game.players.Each(func(c *Player) {
c.Tick()
})
case <-statistics.C:
case <-statistics:
fmt.Printf("%d players | %d packets\n", game.players.Count(), game.server.PacketCount())
game.server.ResetPacketCount()
case <-clean.C:
case <-clean:
game.players.Clean(5 * time.Second)
case <-close:

View File

@ -50,21 +50,14 @@ func (game *Game) Login(data []byte, address *net.UDPAddr) error {
player = game.players.ByAccount(account.ID)
if player != nil {
game.reconnect(player, address)
} else {
game.connect(account, address)
game.server.Send(Login, []byte{Failure}, address)
return ErrAlreadyLoggedIn
}
game.connect(account, address)
return nil
}
func (game *Game) reconnect(player *Player, address *net.UDPAddr) {
player.KeepAlive()
game.players.ChangeAddress(player, address)
game.sendLoginSuccess(player)
player.OnConnect()
}
func (game *Game) connect(account *Account, address *net.UDPAddr) {
player := NewPlayer(address, account, game)
player.authToken = createAuthToken()

View File

@ -48,6 +48,7 @@ func (player *Player) State() []byte {
return state
}
// OnConnect is executed when a connection has been established.
func (player *Player) OnConnect() {
fmt.Printf("%s connected.\n", player.Name)
players := player.game.players
@ -62,6 +63,7 @@ func (player *Player) OnConnect() {
})
}
// OnDisconnect is executed when the connection has been closed.
func (player *Player) OnDisconnect() {
fmt.Printf("%s disconnected.\n", player.Name)
player.game.BroadcastOthers(PlayerRemove, []byte(player.ID), player)

View File

@ -2,92 +2,96 @@ package game
import (
"net"
"sync"
"sync/atomic"
"time"
)
// PlayerManager keeps tracks of all players.
type PlayerManager struct {
players sync.Map
accounts sync.Map
count atomic.Int64
players map[string]*Player
accounts map[string]*Player
count int
}
// NewPlayerManager creates a new player manager.
func NewPlayerManager() *PlayerManager {
return &PlayerManager{}
}
func (m *PlayerManager) Clean(timeout time.Duration) {
now := time.Now()
m.players.Range(func(key, value interface{}) bool {
player := value.(*Player)
if !player.lastPacket.IsZero() && now.After(player.lastPacket.Add(timeout)) {
m.players.Delete(key)
m.accounts.Delete(player.ID)
m.count.Add(-1)
player.OnDisconnect()
}
return true
})
return &PlayerManager{
players: map[string]*Player{},
accounts: map[string]*Player{},
}
}
// Add adds a new player.
func (m *PlayerManager) Add(player *Player) {
m.players.Store(player.address.String(), player)
m.accounts.Store(player.ID, player)
m.count.Add(1)
m.players[player.address.String()] = player
m.accounts[player.ID] = player
m.count++
player.OnConnect()
}
// ChangeAddress changes the address of a player.
func (m *PlayerManager) ChangeAddress(player *Player, address *net.UDPAddr) {
m.players.Delete(player.address.String())
delete(m.players, player.address.String())
player.address = address
m.players.Store(player.address.String(), player)
m.players[player.address.String()] = player
}
// Clean checks for players who haven't responded in the timeout duration and disconnects them.
func (m *PlayerManager) Clean(timeout time.Duration) {
now := time.Now()
for _, player := range m.players {
if player.lastPacket.IsZero() || now.Before(player.lastPacket.Add(timeout)) {
continue
}
m.Remove(player)
}
}
// Remove removes a player.
func (m *PlayerManager) Remove(player *Player) {
delete(m.players, player.address.String())
delete(m.accounts, player.ID)
m.count--
player.OnDisconnect()
}
// Contains tells you whether the address is already a registered client.
func (m *PlayerManager) Contains(address *net.UDPAddr) bool {
_, exists := m.players.Load(address.String())
_, exists := m.players[address.String()]
return exists
}
// Count returns the number of clients.
func (m *PlayerManager) Count() int {
return int(m.count.Load())
return m.count
}
// Each calls the callback function for each client.
func (m *PlayerManager) Each(callback func(*Player)) {
m.players.Range(func(key, value any) bool {
callback(value.(*Player))
return true
})
for _, player := range m.players {
callback(player)
}
}
// ByAddress returns an existing client for the requested address.
func (m *PlayerManager) ByAddress(address *net.UDPAddr) *Player {
obj, exists := m.players.Load(address.String())
player, exists := m.players[address.String()]
if !exists {
return nil
}
return obj.(*Player)
return player
}
// ByAccount returns the player with the given account ID.
func (m *PlayerManager) ByAccount(id string) *Player {
obj, exists := m.accounts.Load(id)
player, exists := m.accounts[id]
if !exists {
return nil
}
return obj.(*Player)
return player
}