// Package engine 主循环:tick 固定 20Hz、render 吃满帧率、两者解耦(架构.md §3.2)。 package engine import ( "context" "time" "mc/internal/crash" ) // Ticker 世界逻辑接口:每 tick 调用,dt 为固定 tick 间隔秒数。 type Ticker interface { Tick(dt float64) } // Renderer 渲染接口:每帧调用;ShouldClose 返回 true 时退出循环。 type Renderer interface { Render() ShouldClose() bool } // NoopRenderer 无渲染占位(服务器),以约 60Hz 空转。 type NoopRenderer struct{} // Render 空实现。 func (NoopRenderer) Render() { time.Sleep(time.Second / 60) } // ShouldClose 永不主动关闭。 func (NoopRenderer) ShouldClose() bool { return false } // Run 主循环:tick goroutine 固定 20Hz;主 goroutine 渲染;ctx 取消或窗口关闭时退出。 func Run(ctx context.Context, tick Ticker, render Renderer) { const tickInterval = time.Second / 20 stop := make(chan struct{}) go func() { defer crash.RecoverGo("engine-tick") // 非主 goroutine 兜底:panic 写崩溃日志(防静默闪退) t := time.NewTicker(tickInterval) defer t.Stop() for { select { case <-ctx.Done(): return case <-stop: return case <-t.C: tick.Tick(tickInterval.Seconds()) } } }() defer close(stop) for { if ctx.Err() != nil || render.ShouldClose() { return } render.Render() } }