Files
2026-10-08 10:43:59 +08:00

606 lines
19 KiB
C#

using System.Media;
namespace TempleRun;
/// <summary>
/// 游戏世界:状态机 + 生成器 + 物理 + 碰撞。
/// 不含任何绘制代码,纯逻辑,便于单独测试。
/// </summary>
public class GameWorld
{
public RunState State = RunState.Ready;
public readonly Player Hero = new();
public readonly List<Obstacle> Obstacles = new();
public readonly List<Coin> Coins = new();
public readonly List<Particle> Particles = new();
public readonly List<SideProp> 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; // 由窗体写入,仅用于调试显示
/// <summary>死亡动画播完才允许重开,避免手快误触</summary>
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);
}
/// <summary>世界向玩家方向滚动:所有实体 Z 减少。</summary>
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;
}
}
/// <summary>生成一组障碍 + 可能附带的金币。保证至少留一条可通行车道。</summary>
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<int>();
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 { }
}
}