fix(game): 崩溃修复(卸载区块实体冻结)+ 立面光照、掉落可见、打击感、水下效果、空手拳头、血条左/饥饿右
- 崩溃(index out of range):实体所在区块卸载后失去碰撞支撑无限下坠到 负坐标,光查询越界 panic——WorldView 增 ChunkLoaded,未加载区块实体冻结; chunk 光查询加越界防御(双保险) - 立面光照:mesh 每面取**邻格**光照(原版光照模型)——此前用方块自身光照 (sky=0)导致悬崖/矿洞立面全黑(泥土竖向黑色根因) - 掉落可见:生成后 1 秒拾取延迟 + 图标加大到 0.45 - 打击感:攻击命中音(block.hit)+ 实体受击白闪(uTint)+ 手持挥动动画 - 水下效果:相机入水蓝色叠加(渲染.md §8 水体) - 空手显示肤色拳头(白羊毛 × 肤色 tint),持方块正常显示 - HUD 布局:血条左下、饥饿右下(经典布局)
This commit is contained in:
@@ -65,6 +65,7 @@ var (
|
||||
optTest = flag.Bool("optionstest", false, "dev 测试:截图前打开设置界面")
|
||||
invTest = flag.Bool("invtest", false, "dev 测试:截图前打开物品栏(E 键界面)并放入若干物品")
|
||||
miniDump = flag.String("minimapdump", "", "dev 诊断:保存一次小地图图像到该路径")
|
||||
waterTest = flag.Bool("underwatertest", false, "dev 测试:截图前置水下渲染状态(验证蓝色叠加)")
|
||||
)
|
||||
|
||||
// clientApp 客户端应用:把会话与渲染粘合起来(engine.Ticker + engine.Renderer)。
|
||||
@@ -120,6 +121,11 @@ type clientApp struct {
|
||||
handHas bool
|
||||
handDirty bool
|
||||
handUVs [6][4]float32
|
||||
handTint [3]float32 // 手持颜色乘数(空手肤色拳 / 持方块 1,1,1)
|
||||
swing float64 // 挥动动画剩余量(左/右键触发,渲染线程衰减)
|
||||
|
||||
// 水下状态(渲染线程读;tick 更新)
|
||||
underwater bool
|
||||
|
||||
// 准星指向方块名(tick 光栅化 CJK → 渲染线程上传,UI还原.md §3.4)
|
||||
targetName string
|
||||
@@ -145,6 +151,7 @@ type clientApp struct {
|
||||
optShot bool // dev 测试:-optionstest 已注入
|
||||
invShot bool // dev 测试:-invtest 已注入
|
||||
miniDumped bool // dev 诊断:-minimapdump 已保存
|
||||
waterShot bool // dev 测试:-underwatertest 已注入
|
||||
}
|
||||
|
||||
// worldMeshJob 待上传网格。
|
||||
@@ -291,7 +298,14 @@ func (a *clientApp) Tick(dt float64) {
|
||||
a.requestCursor(false)
|
||||
return
|
||||
}
|
||||
// 挥动动画触发(打击感:左/右键按下时手部挥动)
|
||||
if a.in.breakDown || a.in.placeDown {
|
||||
a.swing = 1.0
|
||||
}
|
||||
a.in.apply(a.sess)
|
||||
// 水下检测(相机所在方块为水 → 水下渲染效果)
|
||||
camB := a.sess.World.Block(int32(a.sess.Cam.Pos[0]), int32(a.sess.Cam.Pos[1]), int32(a.sess.Cam.Pos[2]))
|
||||
a.underwater = a.sess.Reg.IsFluid(camB) || *waterTest
|
||||
// 右键命中工作台 → 打开合成界面(apply 内 TryOpenCrafting 已置状态)
|
||||
if a.sess.CraftingOpen() && a.screen == 1 {
|
||||
a.screen = 3
|
||||
@@ -457,6 +471,11 @@ func (a *clientApp) injectDevVisuals() {
|
||||
a.requestCursor(false)
|
||||
a.logf("invtest 注入: 打开物品栏")
|
||||
}
|
||||
if *waterTest && !a.waterShot {
|
||||
a.waterShot = true
|
||||
a.underwater = true
|
||||
a.logf("underwatertest 注入: 水下渲染状态")
|
||||
}
|
||||
}
|
||||
|
||||
// handleCraftClick 工作台界面点击:网格/热键栏槽与手持栈交换,输出格执行合成。
|
||||
@@ -708,25 +727,20 @@ func (a *clientApp) requestCursor(captured bool) {
|
||||
func (a *clientApp) computeHand() {
|
||||
a.handInit = true
|
||||
a.handSlot = a.sess.Inv.Selected
|
||||
setEmpty := func() {
|
||||
a.mu.Lock()
|
||||
a.handHas = false
|
||||
a.handDirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
sel := a.sess.Inv.SelectedStack()
|
||||
if sel.Empty() {
|
||||
setEmpty()
|
||||
a.computeFistHand()
|
||||
return
|
||||
}
|
||||
d, ok := a.sess.Items.Get(sel.Name)
|
||||
if !ok || d.Place == "" {
|
||||
setEmpty()
|
||||
// 非可放置物品:同样显示肤色拳(原版按物品模型渲染,后置)
|
||||
a.computeFistHand()
|
||||
return
|
||||
}
|
||||
bid, ok := a.reg.ID(d.Place)
|
||||
if !ok {
|
||||
setEmpty()
|
||||
a.computeFistHand()
|
||||
return
|
||||
}
|
||||
st := block.NewState(bid, 0)
|
||||
@@ -742,6 +756,32 @@ func (a *clientApp) computeHand() {
|
||||
a.mu.Lock()
|
||||
a.handUVs = uvs
|
||||
a.handHas = true
|
||||
a.handTint = [3]float32{1, 1, 1}
|
||||
a.handDirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// newMeshBuilder 创建网格构建器:面光照取邻格光照(原版光照模型,
|
||||
// 光照.md 顶点光照——方块自身 sky=0,用自身光照会让悬崖/矿洞立面全黑)。
|
||||
func newMeshBuilder(reg *block.Registry, uvFn func(string) (u0, v0, u1, v1 float32, ok bool), sess *game.Session) *mesh.Builder {
|
||||
b := mesh.NewBuilder(reg, uvFn)
|
||||
b.SetLight(func(x, y, z int32) (uint8, uint8) {
|
||||
return sess.World.SkyLightUnlocked(x, y, z), sess.World.BlockLightUnlocked(x, y, z)
|
||||
})
|
||||
return b
|
||||
}
|
||||
|
||||
// computeFistHand 空手/非方块物品:肤色拳头(白羊毛纹理 × 肤色 tint,渲染.md §5.2)。
|
||||
func (a *clientApp) computeFistHand() {
|
||||
u0, v0, u1, v1, found := a.uv("block/white_wool")
|
||||
if !found {
|
||||
u0, v0, u1, v1 = 0, 0, 1, 1
|
||||
}
|
||||
uvs := [4]float32{u0, v0, u1, v1}
|
||||
a.mu.Lock()
|
||||
a.handUVs = [6][4]float32{uvs, uvs, uvs, uvs, uvs, uvs}
|
||||
a.handHas = true
|
||||
a.handTint = [3]float32{0.85, 0.65, 0.5} // 肤色
|
||||
a.handDirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
@@ -758,8 +798,8 @@ func (a *clientApp) Render() {
|
||||
}
|
||||
a.cursorReq.Store(-1)
|
||||
}
|
||||
// dev 测试:截图前注入破坏裂纹/实体/ESC/工作台/设置/物品栏(必须在 3D 绘制之前,截图帧才能拍到)
|
||||
if a.frames == 195 && (*crackStage >= 0 || *spawnTest != "" || *escTest || *craftTest || *optTest || *invTest) {
|
||||
// dev 测试:截图前注入破坏裂纹/实体/ESC/工作台/设置/物品栏/水下(必须在 3D 绘制之前,截图帧才能拍到)
|
||||
if a.frames == 195 && (*crackStage >= 0 || *spawnTest != "" || *escTest || *craftTest || *optTest || *invTest || *waterTest) {
|
||||
a.injectDevVisuals()
|
||||
}
|
||||
// 上传 tick 构建好的网格(GL 调用必须在渲染线程)
|
||||
@@ -793,9 +833,9 @@ func (a *clientApp) Render() {
|
||||
}
|
||||
if uv, ok := a.rend.EntityUV(key); ok {
|
||||
if e.Kind == entity.KindItemDrop {
|
||||
a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h, uv)
|
||||
a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h, uv, false)
|
||||
} else {
|
||||
a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h*0.6, uv)
|
||||
a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h*0.6, uv, e.HitFlash > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -808,23 +848,38 @@ func (a *clientApp) Render() {
|
||||
}
|
||||
}
|
||||
// 手持方块上传与绘制(渲染.md §5.2;上传必须是渲染线程)
|
||||
// 挥动动画:左/右键后手部斜向下摆,逐帧重传立方体
|
||||
if a.swing > 0 {
|
||||
a.swing -= 0.12
|
||||
if a.swing < 0 {
|
||||
a.swing = 0
|
||||
}
|
||||
}
|
||||
a.mu.Lock()
|
||||
handDirty, handHas := a.handDirty, a.handHas
|
||||
a.handDirty = false
|
||||
var handUVs [6][4]float32
|
||||
var handTint [3]float32
|
||||
if handHas {
|
||||
handUVs = a.handUVs
|
||||
handTint = a.handTint
|
||||
}
|
||||
a.mu.Unlock()
|
||||
swingPos := [3]float32{0.46, -0.44, -0.66}
|
||||
if a.swing > 0 {
|
||||
swingPos[0] -= float32(a.swing) * 0.22 // 挥动:右移 + 下压
|
||||
swingPos[1] -= float32(a.swing) * 0.18
|
||||
handDirty = true // 挥动期间逐帧重传位置
|
||||
}
|
||||
if handDirty {
|
||||
if handHas {
|
||||
a.rend.SetHandCube(handUVs, [3]float32{0.46, -0.44, -0.66}, 0.16)
|
||||
a.rend.SetHandCube(handUVs, swingPos, 0.16)
|
||||
} else {
|
||||
a.rend.ClearHand()
|
||||
}
|
||||
}
|
||||
if a.screen == 1 {
|
||||
a.rend.DrawHand()
|
||||
a.rend.DrawHand(handTint)
|
||||
}
|
||||
|
||||
// 准星指向方块名上传(tick 光栅化 → 渲染线程上传,UI还原.md §3.4)
|
||||
@@ -852,6 +907,7 @@ func (a *clientApp) Render() {
|
||||
|
||||
// UI pass(UI与输入.md §2:3D 之后、状态隔离)
|
||||
a.uiR.Begin()
|
||||
a.hud.SetUnderwater(a.underwater)
|
||||
mx, my := float32(a.in.cursorX), float32(a.in.cursorY)
|
||||
switch a.screen {
|
||||
case 0:
|
||||
@@ -1255,7 +1311,7 @@ func main() {
|
||||
log: log,
|
||||
lang: lang,
|
||||
remotePlayers: make(map[uint32][3]float64),
|
||||
meshBuilder: mesh.NewBuilder(reg, uvFn),
|
||||
meshBuilder: newMeshBuilder(reg, uvFn, sess),
|
||||
uv: uvFn,
|
||||
handSlot: -1,
|
||||
winW: cfg.Client.WindowWidth,
|
||||
|
||||
@@ -36,7 +36,7 @@ func entityVisual(e *entity.Entity, items *item.Registry) (key string, height fl
|
||||
if !ok {
|
||||
return "", 0
|
||||
}
|
||||
return def.Texture, 0.3
|
||||
return def.Texture, 0.45
|
||||
default:
|
||||
return "", 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user