feat(mpsync): 实体位置广播(EntityMove)与客户端在线玩家显示;mesh 顶点池复用(性能与内存.md §2)

This commit is contained in:
NianGao Dev
2026-08-16 00:05:17 +08:00
parent e70314fb3f
commit 95c8d677ea
10 changed files with 146 additions and 21 deletions

View File

@@ -4,10 +4,32 @@
package mesh
import (
"sync"
"mc/internal/block"
"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。
type Vertex struct {
X, Y, Z float32
@@ -62,6 +84,11 @@ var faceTexture = [6]string{"up", "down", "east", "west", "south", "north"}
// Build 构建区块网格(面剔除:仅当邻格非完全不透明时生成该面,渲染.md §3.1)。
func (b *Builder) Build(c *chunk.Chunk, nb Neighbor) *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()
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
switch {
case b.reg.IsTranslucent(s):
target = &m.Translucent
target = tb
case b.reg.IsCutout(s):
target = &m.Cutout
target = cb
default:
target = &m.Opaque
target = ob
}
wx := int32(x) + c.CX*16
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
}

View File

@@ -26,6 +26,22 @@ func dummyUV(key string) (float32, float32, float32, float32, bool) {
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 顶点四边形)。
func TestSingleBlock(t *testing.T) {
reg := loadReg(t)

View File

@@ -72,6 +72,24 @@ func (h *Hub) SetBlockWorld(x, y, z int32, s block.State) {
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 绝对值。
func absi32(v int32) int32 {
if v < 0 {

View File

@@ -24,6 +24,8 @@ type Handler interface {
OnChunk(data []byte)
// OnBlockChange 方块变更。
OnBlockChange(m netproto.BlockChange)
// OnEntityMove 其他实体移动(多人同步.md §3)。
OnEntityMove(m netproto.EntityMove)
}
// Client TCP 客户端。
@@ -100,6 +102,10 @@ func (c *Client) dispatch(f netproto.Frame) {
if m, err := netproto.DecodeBlockChange(f.Payload); err == nil {
c.handler.OnBlockChange(m)
}
case netproto.MsgEntityMove:
if m, err := netproto.DecodeEntityMove(f.Payload); err == nil {
c.handler.OnEntityMove(m)
}
case netproto.MsgDisconnect:
c.Close()
}

View File

@@ -44,6 +44,7 @@ func newTestHandler() *testHandler {
func (h *testHandler) OnLogin(code uint8) { h.login <- code }
func (h *testHandler) OnChunk(data []byte) {}
func (h *testHandler) OnBlockChange(m netproto.BlockChange) { h.blocks <- m }
func (h *testHandler) OnEntityMove(m netproto.EntityMove) {}
// TestClientRoundtrip 客户端登录 → 发移动 → 收方块变更全链路(多人同步.md §2)。
func TestClientRoundtrip(t *testing.T) {

View File

@@ -317,3 +317,28 @@ func DecodeHandshake(b []byte) (Handshake, error) {
m := Handshake{ProtocolVersion: int(r.Varint()), ClientID: r.Str()}
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
}

View File

@@ -22,6 +22,9 @@ type HUD struct {
// 逻辑分辨率与缩放(UI还原.md §3.1:427×240 基准 × 整数缩放)
baseW, baseH float32
scale float32
// OnlineCount 在线玩家数(联机时由调用方注入;nil 表示单机不显示)
OnlineCount func() int
}
// 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)
// 联机在线人数(多人同步.md §3)
if h.OnlineCount != nil {
r.DrawText(w/2, hgt-56*s, fmt.Sprintf("在线玩家: %d", h.OnlineCount()), 1*s, white)
}
}
// drawItemIcon 在槽位绘制物品图标 + 数量。