feat(mpsync): 实体位置广播(EntityMove)与客户端在线玩家显示;mesh 顶点池复用(性能与内存.md §2)
This commit is contained in:
@@ -57,6 +57,10 @@ type clientApp struct {
|
|||||||
net *netclient.Client
|
net *netclient.Client
|
||||||
log *logx.Logger
|
log *logx.Logger
|
||||||
|
|
||||||
|
// 其他玩家位置(多人同步.md §3;渲染插值后置)
|
||||||
|
remoteMu sync.Mutex
|
||||||
|
remotePlayers map[uint32][3]float64
|
||||||
|
|
||||||
meshBuilder *mesh.Builder
|
meshBuilder *mesh.Builder
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -90,6 +94,11 @@ func (a *clientApp) Tick(dt float64) {
|
|||||||
} else {
|
} else {
|
||||||
a.screen = 1
|
a.screen = 1
|
||||||
a.rend.SetCursorMode(true)
|
a.rend.SetCursorMode(true)
|
||||||
|
a.hud.OnlineCount = func() int {
|
||||||
|
a.remoteMu.Lock()
|
||||||
|
defer a.remoteMu.Unlock()
|
||||||
|
return len(a.remotePlayers) + 1 // 含自己
|
||||||
|
}
|
||||||
a.logf("已连接服务器")
|
a.logf("已连接服务器")
|
||||||
}
|
}
|
||||||
case "quit":
|
case "quit":
|
||||||
@@ -174,6 +183,7 @@ func (a *clientApp) Render() {
|
|||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
for _, j := range pending {
|
for _, j := range pending {
|
||||||
a.rend.UploadChunk(j.cx, j.cz, j.m)
|
a.rend.UploadChunk(j.cx, j.cz, j.m)
|
||||||
|
mesh.Release(j.m) // 上传完毕归还顶点池(性能与内存.md §2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 视图投影
|
// 视图投影
|
||||||
@@ -365,19 +375,20 @@ func main() {
|
|||||||
in := newInputState(rend.Window())
|
in := newInputState(rend.Window())
|
||||||
|
|
||||||
app := &clientApp{
|
app := &clientApp{
|
||||||
sess: sess,
|
sess: sess,
|
||||||
rend: rend,
|
rend: rend,
|
||||||
reg: reg,
|
reg: reg,
|
||||||
in: in,
|
in: in,
|
||||||
uiR: uiR,
|
uiR: uiR,
|
||||||
hud: hud,
|
hud: hud,
|
||||||
titleScreen: titleScreen,
|
titleScreen: titleScreen,
|
||||||
pauseScreen: pauseScreen,
|
pauseScreen: pauseScreen,
|
||||||
log: log,
|
log: log,
|
||||||
meshBuilder: mesh.NewBuilder(reg, uvFn),
|
remotePlayers: make(map[uint32][3]float64),
|
||||||
winW: cfg.Client.WindowWidth,
|
meshBuilder: mesh.NewBuilder(reg, uvFn),
|
||||||
winH: cfg.Client.WindowHeight,
|
winW: cfg.Client.WindowWidth,
|
||||||
lastTime: time.Now(),
|
winH: cfg.Client.WindowHeight,
|
||||||
|
lastTime: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ func (h *mpHandler) OnBlockChange(m netproto.BlockChange) {
|
|||||||
h.app.sess.World.SetBlock(m.X, m.Y, m.Z, block.NewState(m.BlockID, m.Meta))
|
h.app.sess.World.SetBlock(m.X, m.Y, m.Z, block.NewState(m.BlockID, m.Meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnEntityMove 其他玩家移动(多人同步.md §3:客户端存储位置,渲染插值后置)。
|
||||||
|
func (h *mpHandler) OnEntityMove(m netproto.EntityMove) {
|
||||||
|
h.app.remoteMu.Lock()
|
||||||
|
h.app.remotePlayers[m.EntityID] = [3]float64{float64(m.X), float64(m.Y), float64(m.Z)}
|
||||||
|
h.app.remoteMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// dialServer 连接服务器(多人游戏按钮,多人同步.md §2)。
|
// dialServer 连接服务器(多人游戏按钮,多人同步.md §2)。
|
||||||
func dialServer(a *clientApp, addr string, name string) error {
|
func dialServer(a *clientApp, addr string, name string) error {
|
||||||
h := &mpHandler{app: a}
|
h := &mpHandler{app: a}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"mc/internal/engine"
|
"mc/internal/engine"
|
||||||
"mc/internal/logx"
|
"mc/internal/logx"
|
||||||
"mc/internal/mods"
|
"mc/internal/mods"
|
||||||
|
"mc/internal/mpsync"
|
||||||
"mc/internal/netproto"
|
"mc/internal/netproto"
|
||||||
"mc/internal/netserver"
|
"mc/internal/netserver"
|
||||||
"mc/internal/save"
|
"mc/internal/save"
|
||||||
@@ -31,6 +32,7 @@ type serverApp struct {
|
|||||||
reg *block.Registry
|
reg *block.Registry
|
||||||
world *world.World
|
world *world.World
|
||||||
net *netserver.Server
|
net *netserver.Server
|
||||||
|
hub *mpsync.Hub
|
||||||
modReg *mods.Registry
|
modReg *mods.Registry
|
||||||
luaMods []*mods.LuaMod
|
luaMods []*mods.LuaMod
|
||||||
seed int64
|
seed int64
|
||||||
@@ -44,25 +46,27 @@ func (s *serverApp) OnConnect(c *netserver.Conn) {
|
|||||||
s.log.Infof("玩家连接 #%d", c.ID())
|
s.log.Infof("玩家连接 #%d", c.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnHandshake 握手校验(网络协议.md §4:协议版本不匹配拒绝)。
|
// OnHandshake 握手校验(网络协议.md §4:协议版本不匹配拒绝)+ 加入同步中心。
|
||||||
func (s *serverApp) OnHandshake(c *netserver.Conn, hs netproto.Handshake) uint8 {
|
func (s *serverApp) OnHandshake(c *netserver.Conn, hs netproto.Handshake) uint8 {
|
||||||
if hs.ProtocolVersion != 1 {
|
if hs.ProtocolVersion != 1 {
|
||||||
s.log.Warnf("玩家 #%d 协议版本 %d 不匹配,拒绝", c.ID(), hs.ProtocolVersion)
|
s.log.Warnf("玩家 #%d 协议版本 %d 不匹配,拒绝", c.ID(), hs.ProtocolVersion)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
s.log.Infof("玩家 %q 登录(连接 #%d)", hs.ClientID, c.ID())
|
s.log.Infof("玩家 %q 登录(连接 #%d)", hs.ClientID, c.ID())
|
||||||
|
// 登录成功:加入广播集并发送区块(多人同步.md §4)
|
||||||
|
s.hub.AddPlayer(c, 8, 8)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnMove 玩家移动(MVP:仅记录,完整同步见 Todo 8)。
|
// OnMove 玩家移动:广播给其他客户端(多人同步.md §3)。
|
||||||
func (s *serverApp) OnMove(c *netserver.Conn, m netproto.PlayerMove) {
|
func (s *serverApp) OnMove(c *netserver.Conn, m netproto.PlayerMove) {
|
||||||
// TODO(多人同步): 更新玩家实体并广播给视距内其他玩家(多人同步.md §3)
|
s.hub.BroadcastEntityMove(c, uint32(c.ID()), m)
|
||||||
_ = m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnDisconnect 连接断开。
|
// OnDisconnect 连接断开:移出同步中心。
|
||||||
func (s *serverApp) OnDisconnect(c *netserver.Conn) {
|
func (s *serverApp) OnDisconnect(c *netserver.Conn) {
|
||||||
s.log.Infof("玩家 #%d 断开", c.ID())
|
s.log.Infof("玩家 #%d 断开", c.ID())
|
||||||
|
s.hub.RemovePlayer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tick 每 tick(20Hz)服务器逻辑(架构.md §3.2:tick 内不做 IO)。
|
// Tick 每 tick(20Hz)服务器逻辑(架构.md §3.2:tick 内不做 IO)。
|
||||||
@@ -128,6 +132,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app := &serverApp{cfg: cfg, log: log, reg: reg, world: w, seed: seed}
|
app := &serverApp{cfg: cfg, log: log, reg: reg, world: w, seed: seed}
|
||||||
|
app.hub = mpsync.NewHub(w, cfg.Server.ViewDistance)
|
||||||
|
|
||||||
// Mod 加载(设计.md §3.3:扫描 mods/,type=mod 且有入口脚本的执行;热更新.md 由注册表支持)
|
// Mod 加载(设计.md §3.3:扫描 mods/,type=mod 且有入口脚本的执行;热更新.md 由注册表支持)
|
||||||
am := assets.New()
|
am := assets.New()
|
||||||
|
|||||||
@@ -4,10 +4,32 @@
|
|||||||
package mesh
|
package mesh
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
"mc/internal/block"
|
"mc/internal/block"
|
||||||
"mc/internal/chunk"
|
"mc/internal/chunk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// vertexPools 顶点切片池(性能与内存.md §2:热路径零分配——渲染线程不造 mesh 垃圾)。
|
||||||
|
var vertexPools = [3]sync.Pool{
|
||||||
|
{New: func() any { s := make([]Vertex, 0, 4096); return &s }},
|
||||||
|
{New: func() any { s := make([]Vertex, 0, 1024); return &s }},
|
||||||
|
{New: func() any { s := make([]Vertex, 0, 1024); return &s }},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release 归还网格顶点缓冲到池(GPU 上传后调用,性能与内存.md §8 sync.Pool 复位)。
|
||||||
|
func Release(m *ChunkMesh) {
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, s := range [3]*[]Vertex{&m.Opaque, &m.Cutout, &m.Translucent} {
|
||||||
|
if cap(*s) > 0 && cap(*s) <= 1<<20 {
|
||||||
|
vertexPools[i].Put(s)
|
||||||
|
}
|
||||||
|
*s = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vertex 顶点格式(渲染.md §5):位置 + UV + 天空/方块光各 4-bit。
|
// Vertex 顶点格式(渲染.md §5):位置 + UV + 天空/方块光各 4-bit。
|
||||||
type Vertex struct {
|
type Vertex struct {
|
||||||
X, Y, Z float32
|
X, Y, Z float32
|
||||||
@@ -62,6 +84,11 @@ var faceTexture = [6]string{"up", "down", "east", "west", "south", "north"}
|
|||||||
// Build 构建区块网格(面剔除:仅当邻格非完全不透明时生成该面,渲染.md §3.1)。
|
// Build 构建区块网格(面剔除:仅当邻格非完全不透明时生成该面,渲染.md §3.1)。
|
||||||
func (b *Builder) Build(c *chunk.Chunk, nb Neighbor) *ChunkMesh {
|
func (b *Builder) Build(c *chunk.Chunk, nb Neighbor) *ChunkMesh {
|
||||||
m := &ChunkMesh{}
|
m := &ChunkMesh{}
|
||||||
|
// 顶点缓冲从池中取(性能与内存.md §2)
|
||||||
|
ob := vertexPools[0].Get().(*[]Vertex)
|
||||||
|
cb := vertexPools[1].Get().(*[]Vertex)
|
||||||
|
tb := vertexPools[2].Get().(*[]Vertex)
|
||||||
|
*ob, *cb, *tb = (*ob)[:0], (*cb)[:0], (*tb)[:0]
|
||||||
blocks := c.Snapshot()
|
blocks := c.Snapshot()
|
||||||
baseX, baseZ := float32(c.CX*16), float32(c.CZ*16)
|
baseX, baseZ := float32(c.CX*16), float32(c.CZ*16)
|
||||||
|
|
||||||
@@ -75,11 +102,11 @@ func (b *Builder) Build(c *chunk.Chunk, nb Neighbor) *ChunkMesh {
|
|||||||
var target *[]Vertex
|
var target *[]Vertex
|
||||||
switch {
|
switch {
|
||||||
case b.reg.IsTranslucent(s):
|
case b.reg.IsTranslucent(s):
|
||||||
target = &m.Translucent
|
target = tb
|
||||||
case b.reg.IsCutout(s):
|
case b.reg.IsCutout(s):
|
||||||
target = &m.Cutout
|
target = cb
|
||||||
default:
|
default:
|
||||||
target = &m.Opaque
|
target = ob
|
||||||
}
|
}
|
||||||
wx := int32(x) + c.CX*16
|
wx := int32(x) + c.CX*16
|
||||||
wy := int32(y)
|
wy := int32(y)
|
||||||
@@ -119,6 +146,8 @@ func (b *Builder) Build(c *chunk.Chunk, nb Neighbor) *ChunkMesh {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 挂回结果(注意:切片头来自池,调用方上传后必须 Release)
|
||||||
|
m.Opaque, m.Cutout, m.Translucent = *ob, *cb, *tb
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,22 @@ func dummyUV(key string) (float32, float32, float32, float32, bool) {
|
|||||||
return 0, 0, 1, 1, true
|
return 0, 0, 1, 1, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPoolReuse 顶点池复用:Build → Release → 再次 Build 不崩溃且结果正确(性能与内存.md §2)。
|
||||||
|
func TestPoolReuse(t *testing.T) {
|
||||||
|
reg := loadReg(t)
|
||||||
|
stone, _ := reg.ID("stone")
|
||||||
|
c := chunk.New(0, 0)
|
||||||
|
c.Fill(0, 0, 0, block.NewState(stone, 0))
|
||||||
|
b := NewBuilder(reg, dummyUV)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
m := b.Build(c, airNb)
|
||||||
|
if len(m.Opaque) != 24 {
|
||||||
|
t.Fatalf("第 %d 次构建顶点数异常: %d", i, len(m.Opaque))
|
||||||
|
}
|
||||||
|
Release(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSingleBlock 单方块 6 面全生成:24 顶点(6 面 × 4 顶点四边形)。
|
// TestSingleBlock 单方块 6 面全生成:24 顶点(6 面 × 4 顶点四边形)。
|
||||||
func TestSingleBlock(t *testing.T) {
|
func TestSingleBlock(t *testing.T) {
|
||||||
reg := loadReg(t)
|
reg := loadReg(t)
|
||||||
|
|||||||
@@ -72,6 +72,24 @@ func (h *Hub) SetBlockWorld(x, y, z int32, s block.State) {
|
|||||||
h.BroadcastBlockChange(x, y, z, s.ID(), s.Meta())
|
h.BroadcastBlockChange(x, y, z, s.ID(), s.Meta())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BroadcastEntityMove 广播玩家移动给其他客户端(多人同步.md §3:20Hz 增量)。
|
||||||
|
// from 为发送者(不回传给自己)。
|
||||||
|
func (h *Hub) BroadcastEntityMove(from *netserver.Conn, entityID uint32, m netproto.PlayerMove) {
|
||||||
|
msg := netproto.EntityMove{EntityID: entityID, X: m.X, Y: m.Y, Z: m.Z, Yaw: m.Yaw}
|
||||||
|
payload := msg.Encode()
|
||||||
|
h.mu.Lock()
|
||||||
|
clients := make([]*netserver.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
if c != from {
|
||||||
|
clients = append(clients, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
for _, c := range clients {
|
||||||
|
c.Send(netproto.Frame{MsgID: netproto.MsgEntityMove, Payload: payload})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// absi32 绝对值。
|
// absi32 绝对值。
|
||||||
func absi32(v int32) int32 {
|
func absi32(v int32) int32 {
|
||||||
if v < 0 {
|
if v < 0 {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ type Handler interface {
|
|||||||
OnChunk(data []byte)
|
OnChunk(data []byte)
|
||||||
// OnBlockChange 方块变更。
|
// OnBlockChange 方块变更。
|
||||||
OnBlockChange(m netproto.BlockChange)
|
OnBlockChange(m netproto.BlockChange)
|
||||||
|
// OnEntityMove 其他实体移动(多人同步.md §3)。
|
||||||
|
OnEntityMove(m netproto.EntityMove)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client TCP 客户端。
|
// Client TCP 客户端。
|
||||||
@@ -100,6 +102,10 @@ func (c *Client) dispatch(f netproto.Frame) {
|
|||||||
if m, err := netproto.DecodeBlockChange(f.Payload); err == nil {
|
if m, err := netproto.DecodeBlockChange(f.Payload); err == nil {
|
||||||
c.handler.OnBlockChange(m)
|
c.handler.OnBlockChange(m)
|
||||||
}
|
}
|
||||||
|
case netproto.MsgEntityMove:
|
||||||
|
if m, err := netproto.DecodeEntityMove(f.Payload); err == nil {
|
||||||
|
c.handler.OnEntityMove(m)
|
||||||
|
}
|
||||||
case netproto.MsgDisconnect:
|
case netproto.MsgDisconnect:
|
||||||
c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func newTestHandler() *testHandler {
|
|||||||
func (h *testHandler) OnLogin(code uint8) { h.login <- code }
|
func (h *testHandler) OnLogin(code uint8) { h.login <- code }
|
||||||
func (h *testHandler) OnChunk(data []byte) {}
|
func (h *testHandler) OnChunk(data []byte) {}
|
||||||
func (h *testHandler) OnBlockChange(m netproto.BlockChange) { h.blocks <- m }
|
func (h *testHandler) OnBlockChange(m netproto.BlockChange) { h.blocks <- m }
|
||||||
|
func (h *testHandler) OnEntityMove(m netproto.EntityMove) {}
|
||||||
|
|
||||||
// TestClientRoundtrip 客户端登录 → 发移动 → 收方块变更全链路(多人同步.md §2)。
|
// TestClientRoundtrip 客户端登录 → 发移动 → 收方块变更全链路(多人同步.md §2)。
|
||||||
func TestClientRoundtrip(t *testing.T) {
|
func TestClientRoundtrip(t *testing.T) {
|
||||||
|
|||||||
@@ -317,3 +317,28 @@ func DecodeHandshake(b []byte) (Handshake, error) {
|
|||||||
m := Handshake{ProtocolVersion: int(r.Varint()), ClientID: r.Str()}
|
m := Handshake{ProtocolVersion: int(r.Varint()), ClientID: r.Str()}
|
||||||
return m, r.Err
|
return m, r.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EntityMove 实体移动(0x07,20Hz 广播,多人同步.md §3)。
|
||||||
|
type EntityMove struct {
|
||||||
|
EntityID uint32
|
||||||
|
X, Y, Z float32
|
||||||
|
Yaw float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode 编码实体移动。
|
||||||
|
func (m EntityMove) Encode() []byte {
|
||||||
|
w := &Writer{}
|
||||||
|
w.U32(m.EntityID)
|
||||||
|
w.F32(m.X)
|
||||||
|
w.F32(m.Y)
|
||||||
|
w.F32(m.Z)
|
||||||
|
w.F32(m.Yaw)
|
||||||
|
return w.B
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeEntityMove 解码实体移动。
|
||||||
|
func DecodeEntityMove(b []byte) (EntityMove, error) {
|
||||||
|
r := NewReader(b)
|
||||||
|
m := EntityMove{EntityID: r.U32(), X: r.F32(), Y: r.F32(), Z: r.F32(), Yaw: r.F32()}
|
||||||
|
return m, r.Err
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ type HUD struct {
|
|||||||
// 逻辑分辨率与缩放(UI还原.md §3.1:427×240 基准 × 整数缩放)
|
// 逻辑分辨率与缩放(UI还原.md §3.1:427×240 基准 × 整数缩放)
|
||||||
baseW, baseH float32
|
baseW, baseH float32
|
||||||
scale float32
|
scale float32
|
||||||
|
|
||||||
|
// OnlineCount 在线玩家数(联机时由调用方注入;nil 表示单机不显示)
|
||||||
|
OnlineCount func() int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHUD 创建 HUD。
|
// NewHUD 创建 HUD。
|
||||||
@@ -84,6 +87,10 @@ func (h *HUD) Draw(r *Renderer) {
|
|||||||
|
|
||||||
// 坐标调试(后置移除)
|
// 坐标调试(后置移除)
|
||||||
r.DrawText(w/2, hgt-40*s, fmt.Sprintf("XYZ %.1f / %.1f / %.1f", h.sess.Player.Pos[0], h.sess.Player.Pos[1], h.sess.Player.Pos[2]), 1*s, white)
|
r.DrawText(w/2, hgt-40*s, fmt.Sprintf("XYZ %.1f / %.1f / %.1f", h.sess.Player.Pos[0], h.sess.Player.Pos[1], h.sess.Player.Pos[2]), 1*s, white)
|
||||||
|
// 联机在线人数(多人同步.md §3)
|
||||||
|
if h.OnlineCount != nil {
|
||||||
|
r.DrawText(w/2, hgt-56*s, fmt.Sprintf("在线玩家: %d", h.OnlineCount()), 1*s, white)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawItemIcon 在槽位绘制物品图标 + 数量。
|
// drawItemIcon 在槽位绘制物品图标 + 数量。
|
||||||
|
|||||||
Reference in New Issue
Block a user