using System.Media; namespace TempleRun; /// /// 游戏世界:状态机 + 生成器 + 物理 + 碰撞。 /// 不含任何绘制代码,纯逻辑,便于单独测试。 /// public class GameWorld { public RunState State = RunState.Ready; public readonly Player Hero = new(); public readonly List Obstacles = new(); public readonly List Coins = new(); public readonly List Particles = new(); public readonly List Props = new(); // 统计 public float Speed; public float Distance; // 累计前进距离(世界单位) public int CoinCount; public int Best; public int Score => (int)(Distance / 10f) + CoinCount * 10; // 表现层效果 public float Shake; // 屏幕震动强度(像素) public float HitFlash; // 撞击红闪 0..1 public float CoinPop; // 吃金币时的 HUD 弹跳 0..1 public bool Muted; public float Clock; // 累计运行时间(用于 UI 呼吸/闪烁动画) public float Fps; // 由窗体写入,仅用于调试显示 /// 死亡动画播完才允许重开,避免手快误触 public bool CanRestart => _deathDelay <= 0f; private float _nextSpawn; // 距下一组障碍还有多远 private float _nextProp; // 距下一组路边装饰还有多远 private float _deathDelay; // 死亡动画剩余时间 private float _lastCoinSfx; private float _sfxClock; private readonly Random _rng = new(); private readonly string _bestPath; public GameWorld() { try { string dir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TempleRun"); Directory.CreateDirectory(dir); _bestPath = Path.Combine(dir, "best.txt"); if (File.Exists(_bestPath) && int.TryParse(File.ReadAllText(_bestPath), out int b)) Best = b; } catch { _bestPath = null; } Reset(); } // ------------------------------------------------------------------ // 生命周期 // ------------------------------------------------------------------ public void Reset() { Hero.Reset(); Obstacles.Clear(); Coins.Clear(); Particles.Clear(); Props.Clear(); Speed = Cfg.StartSpeed; Distance = 0; CoinCount = 0; Shake = 0; HitFlash = 0; CoinPop = 0; _deathDelay = 0; _nextSpawn = 1400f; _nextProp = 120f; // 预铺一条"石柱长廊",避免开局两侧空荡 for (float z = 400; z < Cfg.FarZ; z += 165f) SpawnProp(z); State = RunState.Ready; } public void Start() { Reset(); State = RunState.Playing; } // ------------------------------------------------------------------ // 输入 // ------------------------------------------------------------------ public bool HandleKey(Keys key) { switch (State) { case RunState.Ready: if (key is Keys.Space or Keys.Enter) { Start(); return true; } break; case RunState.Playing: if (key is Keys.Left or Keys.A) { ShiftLane(-1); return true; } if (key is Keys.Right or Keys.D) { ShiftLane(+1); return true; } if (key is Keys.Up or Keys.W or Keys.Space) { Jump(); return true; } if (key is Keys.Down or Keys.S) { Slide(); return true; } if (key is Keys.P or Keys.Escape) { State = RunState.Paused; return true; } break; case RunState.Paused: if (key is Keys.P or Keys.Escape or Keys.Space) { State = RunState.Playing; return true; } if (key is Keys.R) { Start(); return true; } break; case RunState.GameOver: if (!CanRestart) return true; if (key is Keys.R or Keys.Enter or Keys.Space) { Start(); return true; } break; } if (key is Keys.M) { Muted = !Muted; return true; } return false; } private void ShiftLane(int dir) { if (!Hero.Alive) return; int t = Math.Clamp(Hero.TargetLane + dir, 0, Cfg.LaneCount - 1); if (t != Hero.TargetLane) Hero.TargetLane = t; } private void Jump() { if (!Hero.Alive || !Hero.OnGround) return; Hero.Vy = Cfg.JumpSpeed; Hero.OnGround = false; Hero.Sliding = false; Hero.SlideTimer = 0; SpawnDust(Hero.X, Hero.Y, 0f, 6); } private void Slide() { if (!Hero.Alive) return; // 空中按下 → 快速下落(更顺手) if (!Hero.OnGround) Hero.Vy = Math.Min(Hero.Vy, -420f); Hero.Sliding = true; Hero.SlideTimer = Cfg.SlideDuration; SpawnDust(Hero.X, Hero.Y, 0f, 8); } // ------------------------------------------------------------------ // 主更新 // ------------------------------------------------------------------ public void Update(float dt) { _sfxClock += dt; Clock += dt; if (State == RunState.Paused) return; if (State == RunState.Ready) { // 待机:背景缓慢滚动 + 原地跑,营造氛围 RollWorld(300f * dt); Hero.RunPhase += dt * 7.5f; UpdateParticles(dt); DecayEffects(dt); return; } if (State == RunState.GameOver) { UpdateParticles(dt); DecayEffects(dt); if (_deathDelay > 0) _deathDelay -= dt; return; } // ---------------- 游戏中 ---------------- if (!Hero.Alive) { RollWorld(Speed * dt * 0.25f); UpdateParticles(dt); DecayEffects(dt); _deathDelay -= dt; if (_deathDelay <= 0f) FinishRun(); return; } // 难度递增 float target = Cfg.StartSpeed + Distance * 0.00035f + CoinCount * Cfg.SpeedPerCoin; Speed = Math.Min(Cfg.MaxSpeed, Math.Max(Speed, target)); Speed += Cfg.SpeedAccel * dt; Speed = Math.Min(Speed, Cfg.MaxSpeed); float move = Speed * dt; Distance += move; UpdatePlayer(dt); RollWorld(move); UpdateEntities(dt, move); Generate(move); CheckCollisions(); UpdateParticles(dt); DecayEffects(dt); } private void DecayEffects(float dt) { Shake = Math.Max(0f, Shake - dt * 60f); HitFlash = Math.Max(0f, HitFlash - dt * 2.6f); CoinPop = Math.Max(0f, CoinPop - dt * 4f); } // ------------------------------------------------------------------ // 玩家 // ------------------------------------------------------------------ private void UpdatePlayer(float dt) { float targetX = Cfg.LaneX(Hero.TargetLane); float k = 1f - MathF.Exp(-Cfg.LaneLerp * dt); // 帧率无关的指数插值 float dx = targetX - Hero.X; Hero.X += dx * k; if (Math.Abs(dx) < 0.6f) Hero.X = targetX; // 侧倾:换道时身体倾斜 float wantTilt = Math.Clamp(dx / Cfg.LaneWidth, -1f, 1f) * 0.32f; Hero.Tilt += (wantTilt - Hero.Tilt) * Math.Min(1f, dt * 9f); // 重力 if (!Hero.OnGround || Hero.Vy > 0f) { Hero.Vy -= Cfg.Gravity * dt; Hero.Y += Hero.Vy * dt; if (Hero.Y <= 0f) { Hero.Y = 0f; Hero.Vy = 0f; if (!Hero.OnGround) { Hero.OnGround = true; SpawnDust(Hero.X, 0f, 0f, 8); } } } // 滑铲计时 if (Hero.Sliding) { Hero.SlideTimer -= dt; if (Hero.SlideTimer <= 0f) Hero.Sliding = false; } // 跑动动画(速度越快腿摆得越快) Hero.RunPhase += dt * (4.2f + Speed / 165f); // 跑动扬尘 if (Hero.OnGround && !Hero.Sliding && _rng.NextDouble() < dt * 26) SpawnDust(Hero.X + (float)(_rng.NextDouble() - 0.5) * 30f, 0f, -8f, 1); } /// 世界向玩家方向滚动:所有实体 Z 减少。 private void RollWorld(float move) { Hero.X += 0f; for (int i = Obstacles.Count - 1; i >= 0; i--) Obstacles[i].Z -= move; for (int i = Coins.Count - 1; i >= 0; i--) Coins[i].Z -= move; for (int i = Props.Count - 1; i >= 0; i--) Props[i].Z -= move; for (int i = Particles.Count - 1; i >= 0; i--) Particles[i].Z -= move; } private void UpdateEntities(float dt, float move) { // 金币旋转 + 回收 for (int i = Coins.Count - 1; i >= 0; i--) { var c = Coins[i]; c.Phase += dt * 5.5f; if (c.Taken || c.Z < -260f) Coins.RemoveAt(i); } // 障碍回收 for (int i = Obstacles.Count - 1; i >= 0; i--) if (Obstacles[i].Z < -300f) Obstacles.RemoveAt(i); // 装饰回收 for (int i = Props.Count - 1; i >= 0; i--) if (Props[i].Z < -300f) Props.RemoveAt(i); } // ------------------------------------------------------------------ // 生成器 // ------------------------------------------------------------------ private void Generate(float move) { _nextSpawn -= move; if (_nextSpawn <= 0f) { SpawnRow(Cfg.FarZ); float sec = (float)(Cfg.SpawnIntervalMin + _rng.NextDouble() * (Cfg.SpawnIntervalMax - Cfg.SpawnIntervalMin)); // 速度越快,按"时间间隔"换算成距离间隔,保证反应时间一致 _nextSpawn = Math.Max(420f, sec * Speed * 0.62f); } _nextProp -= move; if (_nextProp <= 0f) { SpawnProp(Cfg.FarZ); _nextProp = 130f + (float)_rng.NextDouble() * 150f; } } /// 生成一组障碍 + 可能附带的金币。保证至少留一条可通行车道。 private void SpawnRow(float z) { int laneCount = _rng.NextDouble() < 0.78 ? 1 : 2; // 1~2 条道放障碍 var lanes = new[] { 0, 1, 2 }; // 洗牌 for (int i = lanes.Length - 1; i > 0; i--) { int j = _rng.Next(i + 1); (lanes[i], lanes[j]) = (lanes[j], lanes[i]); } var used = new List(); for (int i = 0; i < laneCount; i++) { int lane = lanes[i]; used.Add(lane); ObstacleKind kind; double r = _rng.NextDouble(); // 两条道同时出现时,避免双"整道墙"把路堵死(剩 1 条空道仍可走,允许) kind = r < 0.40 ? ObstacleKind.Low : r < 0.72 ? ObstacleKind.High : ObstacleKind.Full; Obstacles.Add(new Obstacle { Z = z, Lane = lane, Kind = kind }); } // 在空道上撒一串金币 if (_rng.NextDouble() < 0.72) { int freeLane = -1; for (int i = 0; i < Cfg.LaneCount; i++) if (!used.Contains(i)) { freeLane = i; break; } if (freeLane >= 0) { int n = 3 + _rng.Next(4); float dz = 52f; // 金币以本组障碍为中心分布,稍微错开一点更自然 float start = z - (n - 1) * dz * 0.5f + (float)(_rng.NextDouble() - 0.5) * 40f; for (int i = 0; i < n; i++) { Coins.Add(new Coin { Z = start + i * dz, Lane = freeLane, Phase = i * 0.6f }); } } } } private void SpawnProp(float z) { int side = _rng.NextDouble() < 0.5 ? -1 : 1; int type = _rng.Next(100) switch { < 42 => 0, // 石柱 < 62 => 1, // 火把 < 90 => 2, // 枯树 _ => 3 // 碎石 }; float h = type switch { 0 => 150f + (float)_rng.NextDouble() * 90f, 1 => 92f, 2 => 130f + (float)_rng.NextDouble() * 70f, _ => 26f + (float)_rng.NextDouble() * 22f }; Props.Add(new SideProp { Z = z, Side = side, Type = type, Height = h, Phase = (float)_rng.NextDouble() * 6.28f }); } // ------------------------------------------------------------------ // 碰撞 // ------------------------------------------------------------------ private void CheckCollisions() { float pNear = -Cfg.PlayerHalfDepth; float pFar = Cfg.PlayerHalfDepth; float py0 = Hero.Y + 5f; // 判定区间略微收缩,手感宽容 float py1 = Hero.Y + Hero.Height - 5f; float px = Hero.X; const float halfBodyW = 15f; // --- 障碍 --- for (int i = 0; i < Obstacles.Count; i++) { var o = Obstacles[i]; float oNear = o.Z - o.Depth * 0.5f; float oFar = o.Z + o.Depth * 0.5f; if (oFar < pNear || oNear > pFar) continue; // Z 不重叠 if (Math.Abs(px - o.CenterX) > o.Width * 0.5f + halfBodyW) continue; // 不在同一道 // 高度区间是否相交 bool hit = py0 < o.TopY && py1 > o.BottomY; //"整道墙"永远相交 if (o.Kind == ObstacleKind.Full) hit = true; if (hit) { Kill(o); return; } if (!o.Scored && o.Z < 0f) { o.Scored = true; // 成功躲避的微小正反馈 Shake = Math.Max(Shake, 1.2f); } } // --- 金币 --- float coinNear = -34f, coinFar = 34f; for (int i = 0; i < Coins.Count; i++) { var c = Coins[i]; if (c.Taken) continue; if (c.Z < coinNear || c.Z > coinFar) continue; if (Math.Abs(px - c.CenterX) > Cfg.LaneWidth * 0.45f) continue; // 金币在 36 高度,跳跃或站立都能吃到,滑铲吃不到 float cy = c.Y; if (cy < py0 - 22f || cy > py1 + 22f) continue; c.Taken = true; CoinCount++; CoinPop = 1f; Speed = Math.Min(Cfg.MaxSpeed, Speed + Cfg.SpeedPerCoin * 0.35f); SpawnCoinBurst(c.CenterX, c.Y, c.Z); PlayCoinSfx(); } } private void Kill(Obstacle o) { if (!Hero.Alive) return; Hero.Alive = false; Hero.Sliding = false; _deathDelay = 0.85f; Shake = 16f; HitFlash = 1f; SpawnDebris(o.CenterX, Hero.Y + 26f, 0f); PlayHitSfx(); } private void FinishRun() { State = RunState.GameOver; if (Score > Best) { Best = Score; try { if (_bestPath != null) File.WriteAllText(_bestPath, Best.ToString()); } catch { /* 忽略写盘失败 */ } } } // ------------------------------------------------------------------ // 粒子 // ------------------------------------------------------------------ public void AddParticle(Particle p) => Particles.Add(p); private void SpawnDust(float x, float y, float z, int count) { for (int i = 0; i < count; i++) { Particles.Add(new Particle { X = x + (float)(_rng.NextDouble() - 0.5) * 26f, Y = y + (float)_rng.NextDouble() * 8f, Z = z + (float)(_rng.NextDouble() - 0.5) * 20f, Vx = (float)(_rng.NextDouble() - 0.5) * 60f, Vy = 20f + (float)_rng.NextDouble() * 55f, Vz = -30f - (float)_rng.NextDouble() * 40f, MaxLife = 0.35f + (float)_rng.NextDouble() * 0.25f, Life = 0f, Size = 2.4f + (float)_rng.NextDouble() * 3.2f, GravityScale = 0.4f, Color = Color.FromArgb(196, 172, 140) }); } } private void SpawnCoinBurst(float x, float y, float z) { for (int i = 0; i < 10; i++) { float a = (float)(_rng.NextDouble() * Math.PI * 2); float sp = 40f + (float)_rng.NextDouble() * 90f; Particles.Add(new Particle { X = x, Y = y, Z = z, Vx = (float)Math.Cos(a) * sp, Vy = 50f + (float)_rng.NextDouble() * 100f, Vz = (float)Math.Sin(a) * sp * 0.6f, MaxLife = 0.45f + (float)_rng.NextDouble() * 0.3f, Size = 2.8f + (float)_rng.NextDouble() * 3.4f, GravityScale = 0.8f, Color = _rng.NextDouble() < 0.5 ? Cfg.Gold : Color.FromArgb(255, 240, 170) }); } } private void SpawnDebris(float x, float y, float z) { for (int i = 0; i < 26; i++) { float a = (float)(_rng.NextDouble() * Math.PI * 2); float sp = 60f + (float)_rng.NextDouble() * 190f; Particles.Add(new Particle { X = x, Y = y, Z = z, Vx = (float)Math.Cos(a) * sp, Vy = 40f + (float)_rng.NextDouble() * 210f, Vz = (float)Math.Sin(a) * sp, MaxLife = 0.6f + (float)_rng.NextDouble() * 0.5f, Size = 3f + (float)_rng.NextDouble() * 5f, GravityScale = 1f, Color = _rng.NextDouble() < 0.5 ? Color.FromArgb(210, 90, 60) : Color.FromArgb(150, 140, 130) }); } } private void UpdateParticles(float dt) { for (int i = Particles.Count - 1; i >= 0; i--) { var p = Particles[i]; p.Life += dt; if (p.Life >= p.MaxLife) { Particles.RemoveAt(i); continue; } p.X += p.Vx * dt; p.Y += p.Vy * dt; p.Z += p.Vz * dt; p.Vy -= Cfg.Gravity * p.GravityScale * dt; if (p.Y < 0f) { p.Y = 0f; p.Vy *= -0.32f; // 轻微反弹 p.Vx *= 0.6f; p.Vz *= 0.6f; } } } // ------------------------------------------------------------------ // 音效(系统提示音,零依赖;M 键静音) // ------------------------------------------------------------------ private void PlayCoinSfx() { if (Muted || _sfxClock - _lastCoinSfx < 0.07f) return; _lastCoinSfx = _sfxClock; try { SystemSounds.Asterisk.Play(); } catch { } } private void PlayHitSfx() { if (Muted) return; try { SystemSounds.Hand.Play(); } catch { } } }