diff --git a/client/network/Login.gd b/client/network/Login.gd index f06c5c8..31c35f7 100644 --- a/client/network/Login.gd +++ b/client/network/Login.gd @@ -1,8 +1,11 @@ extends PacketHandler var auth_token: String +var instance_id := OS.get_process_id() % 4 +var username := "user%d" % instance_id func _ready(): + DisplayServer.window_set_title(username) send_login() func handle_packet(data: PackedByteArray, _peer: PacketPeer): @@ -17,10 +20,11 @@ func handle_packet(data: PackedByteArray, _peer: PacketPeer): func send_login(): if is_logged_in(): return - + + var password := "password" var buffer := StreamPeerBuffer.new() buffer.put_8(Packet.LOGIN) - buffer.put_data(JSON.stringify(["user1", "password"]).to_utf8_buffer()) + buffer.put_data(JSON.stringify([username, password]).to_utf8_buffer()) %Client.socket.put_packet(buffer.data_array) print("[Client] Connecting...") diff --git a/client/network/PlayerState.gd b/client/network/PlayerState.gd index 4a707a5..7fb535d 100644 --- a/client/network/PlayerState.gd +++ b/client/network/PlayerState.gd @@ -17,9 +17,14 @@ func handle_packet(data: PackedByteArray, _peer: PacketPeer): print(server_position) var player := spawn_player() - player.name = player_name player.position = server_position - Global.player = player + player.set_character_name(player_name) + + if false: + Global.player = player + var controller := PlayerController.new() + controller.character = Global.player + Global.player.add_child(controller) func spawn_player() -> Player: var player = player_scene.instantiate() diff --git a/client/player/Player.gd b/client/player/Player.gd index bf3bea5..629bfb7 100644 --- a/client/player/Player.gd +++ b/client/player/Player.gd @@ -1,7 +1,6 @@ class_name Player extends Character -func _ready(): - var controller := PlayerController.new() - controller.character = self - add_child(controller) \ No newline at end of file +func set_character_name(new_name: String): + name = new_name + get_node("Label").text = name \ No newline at end of file diff --git a/client/player/Player.tscn b/client/player/Player.tscn index fb7a007..715e433 100644 --- a/client/player/Player.tscn +++ b/client/player/Player.tscn @@ -1,8 +1,7 @@ -[gd_scene load_steps=6 format=3 uid="uid://2lcnu3dy54lx"] +[gd_scene load_steps=5 format=3 uid="uid://2lcnu3dy54lx"] [ext_resource type="Script" path="res://player/Player.gd" id="1_8gebs"] [ext_resource type="PackedScene" uid="uid://2bbycjulf00g" path="res://character/health/HealthComponent.tscn" id="2_np5ag"] -[ext_resource type="Script" path="res://player/controller/PlayerController.gd" id="3_oox5k"] [sub_resource type="PrismMesh" id="PrismMesh_y7abh"] size = Vector3(0.5, 1.6, 0.5) @@ -29,6 +28,7 @@ shape = SubResource("CapsuleShape3D_2f50n") [node name="Health" parent="." instance=ExtResource("2_np5ag")] -[node name="Controller" type="Node" parent="." node_paths=PackedStringArray("character")] -script = ExtResource("3_oox5k") -character = NodePath("..") +[node name="Label" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0) +billboard = 1 +text = "ABC" diff --git a/client/world/Client.tscn b/client/world/Client.tscn index 5e5da19..4512ee7 100644 --- a/client/world/Client.tscn +++ b/client/world/Client.tscn @@ -68,10 +68,10 @@ camera_attributes = ExtResource("9_w4cdu") [node name="Trees" type="Node3D" parent="World"] [node name="Tree" parent="World/Trees" instance=ExtResource("11_wlyv1")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, -4.64839) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, -7.57074) [node name="Tree2" parent="World/Trees" instance=ExtResource("11_wlyv1")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, 5.35161) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.11323, 0, 7.80548) [node name="Enemies" type="Node3D" parent="World"] @@ -118,6 +118,7 @@ follow_speed = 5.0 [node name="PostProcessing" type="MeshInstance3D" parent="Viewport/SubViewport/CameraPivot/Camera"] unique_name_in_owner = true +visible = false extra_cull_margin = 16384.0 mesh = SubResource("QuadMesh_7yiqd") diff --git a/server/game/Client.go b/server/game/Client.go deleted file mode 100644 index a15267a..0000000 --- a/server/game/Client.go +++ /dev/null @@ -1,31 +0,0 @@ -package game - -import ( - "net" - "time" -) - -// Client represents a logged in client. -type Client struct { - address *net.UDPAddr - lastPacket time.Time - authToken string - player Player -} - -// NewClient creates a new client. -func NewClient(address *net.UDPAddr) *Client { - return &Client{ - address: address, - } -} - -// String shows the client address. -func (c *Client) String() string { - return c.address.String() -} - -// KeepAlive sets the last packet time to now. -func (c *Client) KeepAlive() { - c.lastPacket = time.Now() -} diff --git a/server/game/Game.go b/server/game/Game.go new file mode 100644 index 0000000..9937978 --- /dev/null +++ b/server/game/Game.go @@ -0,0 +1,67 @@ +package game + +import ( + "fmt" + "os" + "os/signal" + "server/game/packet" + "time" +) + +// Game represents the entire state of the game server. +type Game struct { + server *Server + players *PlayerManager +} + +// New creates a new game. +func New() *Game { + return &Game{ + server: NewServer(), + players: NewPlayerManager(), + } +} + +// Run starts all game systems. +func (game *Game) Run() { + game.start() + close := make(chan os.Signal, 1) + signal.Notify(close, os.Interrupt) + <-close +} + +// start starts all game systems. +func (game *Game) start() { + go game.network() + go game.physics() + go game.statistics() +} + +// network will listen for new packets and process them. +func (game *Game) network() { + game.server.SetHandler(packet.Ping, game.Ping) + game.server.SetHandler(packet.Login, game.Login) + game.server.Run(4242) +} + +// physics periodically runs the Tick function for each player. +func (game *Game) physics() { + updater := time.NewTicker(20 * time.Millisecond) + + for range updater.C { + game.players.Each(func(c *Player) bool { + c.Tick() + return true + }) + } +} + +// statistics periodically shows server statistics on the command line. +func (game *Game) statistics() { + ticker := time.NewTicker(time.Second) + + for range ticker.C { + fmt.Printf("%d players | %d packets\n", game.players.Count(), game.server.PacketCount()) + game.server.ResetPacketCount() + } +} diff --git a/server/core/Handler.go b/server/game/Handler.go similarity index 90% rename from server/core/Handler.go rename to server/game/Handler.go index fe0b335..071e982 100644 --- a/server/core/Handler.go +++ b/server/game/Handler.go @@ -1,4 +1,4 @@ -package core +package game import "net" diff --git a/server/game/Login.go b/server/game/Login.go index bf9e993..5e863cb 100644 --- a/server/game/Login.go +++ b/server/game/Login.go @@ -3,70 +3,78 @@ package game import ( "crypto/rand" "encoding/base64" - "encoding/binary" "encoding/json" "errors" - "fmt" - "math" "net" - "server/core" - "server/packet" + "server/game/packet" ) var ( - Clients = NewManager() LoginSuccess = []byte{0} LoginFailure = []byte{1} ) // Login checks the account credentials and gives a network peer access to an account. -func Login(data []byte, address *net.UDPAddr, server *core.Server) error { - var loginRequest [2]string - err := json.Unmarshal(data, &loginRequest) +func (game *Game) Login(data []byte, address *net.UDPAddr, server *Server) error { + username, password, err := getLoginData(data) if err != nil { server.Send(packet.Login, LoginFailure, address) return err } - username := loginRequest[0] - password := loginRequest[1] - if password != "password" { server.Send(packet.Login, LoginFailure, address) return errors.New("login failure") } - randomBytes := make([]byte, 32) - _, err = rand.Read(randomBytes) + player := game.players.Get(address) + player.AuthToken = createAuthToken() + player.Name = username + player.KeepAlive() - if err != nil { - server.Send(packet.Login, LoginFailure, address) - return err + if username == "user0" { + player.Position.X = 5.0 } - client := Clients.Get(address) - client.KeepAlive() - client.authToken = base64.StdEncoding.EncodeToString(randomBytes) - client.player.Name = username - client.player.Position.X = 5.0 - playerState := []byte(client.player.Name + "\u0000") - playerState = appendVector3(playerState, client.player.Position) + if username == "user1" { + player.Position.X = -5.0 + } - server.Send(packet.Login, append(LoginSuccess, []byte(client.authToken)...), address) - server.Send(packet.PlayerState, playerState, address) + if username == "user2" { + player.Position.Z = -5.0 + } + + if username == "user3" { + player.Position.Z = 5.0 + } + + server.Send(packet.Login, append(LoginSuccess, []byte(player.AuthToken)...), address) + player.OnConnect() + + game.players.Each(func(other *Player) bool { + server.Send(packet.PlayerState, other.State(), address) + return true + }) - fmt.Printf("%s logged in.\n", username) return nil } -func appendVector3(data []byte, vector Vector3) []byte { - bits := math.Float32bits(vector.X) - data = binary.LittleEndian.AppendUint32(data, bits) +func getLoginData(data []byte) (string, string, error) { + loginRequest := [2]string{} + err := json.Unmarshal(data, &loginRequest) - bits = math.Float32bits(vector.Y) - data = binary.LittleEndian.AppendUint32(data, bits) + if err != nil { + return "", "", err + } - bits = math.Float32bits(vector.Z) - return binary.LittleEndian.AppendUint32(data, bits) + username := loginRequest[0] + password := loginRequest[1] + return username, password, nil +} + +func createAuthToken() string { + randomBytes := make([]byte, 32) + rand.Read(randomBytes) + return base64.StdEncoding.EncodeToString(randomBytes) } diff --git a/server/game/Manager.go b/server/game/Manager.go deleted file mode 100644 index 79956e8..0000000 --- a/server/game/Manager.go +++ /dev/null @@ -1,68 +0,0 @@ -package game - -import ( - "net" - "sync" - "sync/atomic" - "time" -) - -// Manager keeps tracks of all player connections. -type Manager struct { - clients sync.Map - count atomic.Int64 -} - -// NewManager creates a new manager. -func NewManager() *Manager { - manager := &Manager{} - timeout := 5 * time.Second - interval := time.Second - - go func() { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for range ticker.C { - now := time.Now() - - manager.clients.Range(func(key, value interface{}) bool { - item := value.(*Client) - - if !item.lastPacket.IsZero() && now.After(item.lastPacket.Add(timeout)) { - manager.clients.Delete(key) - manager.count.Add(-1) - } - - return true - }) - } - }() - - return manager -} - -// Get either returns a new or existing client for the requested address. -func (m *Manager) Get(addr *net.UDPAddr) *Client { - obj, exists := m.clients.Load(addr.String()) - - if exists { - return obj.(*Client) - } - - client := NewClient(addr) - m.clients.Store(addr.String(), client) - m.count.Add(1) - return client -} - -// Contains tells you whether the address is already a registered client. -func (m *Manager) Contains(addr *net.UDPAddr) bool { - _, exists := m.clients.Load(addr.String()) - return exists -} - -// Count returns the number of clients. -func (m *Manager) Count() int { - return int(m.count.Load()) -} diff --git a/server/game/Ping.go b/server/game/Ping.go index a0946c1..8aa4d57 100644 --- a/server/game/Ping.go +++ b/server/game/Ping.go @@ -2,16 +2,15 @@ package game import ( "net" - "server/core" - "server/packet" + "server/game/packet" ) // Ping is used as a heartbeat and latency check. -func Ping(data []byte, address *net.UDPAddr, server *core.Server) error { +func (game *Game) Ping(data []byte, address *net.UDPAddr, server *Server) error { server.Send(packet.Ping, data, address) - if Clients.Contains(address) { - Clients.Get(address).KeepAlive() + if game.players.Contains(address) { + game.players.Get(address).KeepAlive() } return nil diff --git a/server/game/Player.go b/server/game/Player.go index ea1f930..2918e1c 100644 --- a/server/game/Player.go +++ b/server/game/Player.go @@ -1,6 +1,53 @@ package game +import ( + "fmt" + "net" + "time" +) + +// Player represents a logged in client. type Player struct { - Name string `json:"name"` - Position Vector3 `json:"position"` + Name string `json:"name"` + Position Vector3 `json:"position"` + AuthToken string + address *net.UDPAddr + lastPacket time.Time +} + +// NewClient creates a new client. +func NewClient(address *net.UDPAddr) *Player { + return &Player{ + address: address, + } +} + +// String shows the client address. +func (c *Player) String() string { + return c.address.String() +} + +// KeepAlive sets the last packet time to now. +func (c *Player) KeepAlive() { + c.lastPacket = time.Now() +} + +// Tick is run on every tick. +func (c *Player) Tick() { + // ... +} + +// State returns the player state (name and position). +func (player *Player) State() []byte { + state := []byte(player.Name + "\u0000") + state = appendVector3(state, player.Position) + return state +} + +func (player *Player) OnConnect() { + fmt.Printf("%s connected.\n", player.Name) +} + +func (player *Player) OnDisconnect() { + fmt.Printf("%s disconnected.\n", player.Name) } diff --git a/server/game/PlayerManager.go b/server/game/PlayerManager.go new file mode 100644 index 0000000..568267a --- /dev/null +++ b/server/game/PlayerManager.go @@ -0,0 +1,76 @@ +package game + +import ( + "net" + "sync" + "sync/atomic" + "time" +) + +// PlayerManager keeps tracks of all players. +type PlayerManager struct { + players sync.Map + count atomic.Int64 +} + +// NewPlayerManager creates a new player manager. +func NewPlayerManager() *PlayerManager { + m := &PlayerManager{} + timeout := 5 * time.Second + interval := time.Second + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for range ticker.C { + now := time.Now() + + m.players.Range(func(key, value interface{}) bool { + player := value.(*Player) + + if !player.lastPacket.IsZero() && now.After(player.lastPacket.Add(timeout)) { + player.OnDisconnect() + m.players.Delete(key) + m.count.Add(-1) + } + + return true + }) + } + }() + + return m +} + +// Contains tells you whether the address is already a registered client. +func (m *PlayerManager) Contains(addr *net.UDPAddr) bool { + _, exists := m.players.Load(addr.String()) + return exists +} + +// Count returns the number of clients. +func (m *PlayerManager) Count() int { + return int(m.count.Load()) +} + +// Each calls the callback function for each client. +func (m *PlayerManager) Each(callback func(*Player) bool) { + m.players.Range(func(key, value any) bool { + return callback(value.(*Player)) + }) +} + +// Get either returns a new or existing client for the requested address. +func (m *PlayerManager) Get(addr *net.UDPAddr) *Player { + obj, exists := m.players.Load(addr.String()) + + if exists { + return obj.(*Player) + } + + client := NewClient(addr) + m.players.Store(addr.String(), client) + m.count.Add(1) + return client +} diff --git a/server/core/Server.go b/server/game/Server.go similarity index 94% rename from server/core/Server.go rename to server/game/Server.go index 224223c..df39e86 100644 --- a/server/core/Server.go +++ b/server/game/Server.go @@ -1,4 +1,4 @@ -package core +package game import ( "fmt" @@ -12,8 +12,8 @@ type Server struct { packetCount int } -// New creates a new server. -func New() *Server { +// NewServer creates a new server. +func NewServer() *Server { return &Server{} } @@ -66,7 +66,7 @@ func listen(port int) *net.UDPConn { // read is a blocking call which will read incoming packets and handle them. func (s *Server) read() { - buffer := make([]byte, 4096) + buffer := make([]byte, 16384) for { n, addr, err := s.socket.ReadFromUDP(buffer) diff --git a/server/game/Vector3.go b/server/game/Vector3.go index fec244b..81da73f 100644 --- a/server/game/Vector3.go +++ b/server/game/Vector3.go @@ -1,7 +1,25 @@ package game +import ( + "encoding/binary" + "math" +) + +// Vector3 represents a 3-dimensional vector using 32-bit float precision. type Vector3 struct { X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` } + +// appendVector3 adds the raw bits of the vector to the given byte slice in XYZ order. +func appendVector3(data []byte, vector Vector3) []byte { + bits := math.Float32bits(vector.X) + data = binary.LittleEndian.AppendUint32(data, bits) + + bits = math.Float32bits(vector.Y) + data = binary.LittleEndian.AppendUint32(data, bits) + + bits = math.Float32bits(vector.Z) + return binary.LittleEndian.AppendUint32(data, bits) +} diff --git a/server/packet/packet.go b/server/game/packet/packet.go similarity index 100% rename from server/packet/packet.go rename to server/game/packet/packet.go diff --git a/server/main.go b/server/main.go index f3f47c9..e1afe9d 100644 --- a/server/main.go +++ b/server/main.go @@ -1,29 +1,9 @@ package main import ( - "fmt" - "server/core" "server/game" - "server/packet" - "time" ) func main() { - // Init server - server := core.New() - server.SetHandler(packet.Ping, game.Ping) - server.SetHandler(packet.Login, game.Login) - - // Show statistics - ticker := time.NewTicker(time.Second) - - go func() { - for range ticker.C { - fmt.Printf("%d packets per second, %d clients\n", server.PacketCount(), game.Clients.Count()) - server.ResetPacketCount() - } - }() - - // Start listening - server.Run(4242) + game.New().Run() }