first commit
This commit is contained in:
@@ -0,0 +1,605 @@
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user