first commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using System.Diagnostics;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
namespace TempleRun;
|
||||
|
||||
/// <summary>
|
||||
/// 主窗体:负责游戏循环(定时器 + 高精度计时)、键盘输入、双缓冲绘制调度。
|
||||
/// </summary>
|
||||
public class GameForm : Form
|
||||
{
|
||||
private readonly GameWorld _world = new();
|
||||
private readonly System.Windows.Forms.Timer _timer;
|
||||
private readonly Stopwatch _watch = Stopwatch.StartNew();
|
||||
|
||||
private double _lastTime;
|
||||
private double _fpsAccum;
|
||||
private int _frameCount;
|
||||
private bool _showFps;
|
||||
|
||||
public GameForm()
|
||||
{
|
||||
Text = "神庙逃亡 · Temple Run (WinForms)";
|
||||
ClientSize = new Size(1024, 720);
|
||||
MinimumSize = new Size(760, 540);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
BackColor = Color.FromArgb(12, 12, 20);
|
||||
DoubleBuffered = true;
|
||||
ResizeRedraw = true;
|
||||
AutoScaleMode = AutoScaleMode.None;
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint
|
||||
| ControlStyles.UserPaint
|
||||
| ControlStyles.OptimizedDoubleBuffer, true);
|
||||
|
||||
try { Icon = BuildIcon(); } catch { /* 图标失败不影响运行 */ }
|
||||
|
||||
_lastTime = _watch.Elapsed.TotalSeconds;
|
||||
|
||||
_timer = new System.Windows.Forms.Timer { Interval = 15 }; // ≈66Hz
|
||||
_timer.Tick += OnTick;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 主循环
|
||||
private void OnTick(object sender, EventArgs e)
|
||||
{
|
||||
double now = _watch.Elapsed.TotalSeconds;
|
||||
float dt = (float)(now - _lastTime);
|
||||
_lastTime = now;
|
||||
|
||||
if (dt <= 0f) return;
|
||||
if (dt > 0.06f) dt = 0.06f; // 卡顿/拖动窗口后不产生穿模
|
||||
|
||||
// FPS 统计
|
||||
_frameCount++;
|
||||
_fpsAccum += dt;
|
||||
if (_fpsAccum >= 0.5)
|
||||
{
|
||||
_world.Fps = (float)(_frameCount / _fpsAccum);
|
||||
_frameCount = 0;
|
||||
_fpsAccum = 0;
|
||||
}
|
||||
|
||||
_world.Update(dt);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
protected override void OnPaint(PaintEventArgs e)
|
||||
{
|
||||
GameRenderer.Draw(e.Graphics, ClientSize.Width, ClientSize.Height, _world, _showFps);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 输入
|
||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||
{
|
||||
Keys key = keyData & Keys.KeyCode;
|
||||
|
||||
if (key == Keys.F1) { _showFps = !_showFps; Invalidate(); return true; }
|
||||
if (key == Keys.F11)
|
||||
{
|
||||
WindowState = WindowState == FormWindowState.Maximized
|
||||
? FormWindowState.Normal : FormWindowState.Maximized;
|
||||
return true;
|
||||
}
|
||||
if (key == Keys.F12) { _world.Muted = !_world.Muted; return true; }
|
||||
|
||||
if (_world.HandleKey(key)) return true;
|
||||
|
||||
return base.ProcessCmdKey(ref msg, keyData);
|
||||
}
|
||||
|
||||
protected override void OnMouseClick(MouseEventArgs e)
|
||||
{
|
||||
base.OnMouseClick(e);
|
||||
// 鼠标左键也能触发"开始/重开",方便触屏设备
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
if (_world.State == RunState.Ready) _world.Start();
|
||||
else if (_world.State == RunState.GameOver && _world.CanRestart)
|
||||
{
|
||||
_world.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosed(FormClosedEventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
GameRenderer.ResetCache();
|
||||
base.OnFormClosed(e);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 图标
|
||||
private static Icon BuildIcon()
|
||||
{
|
||||
using var bmp = new Bitmap(32, 32);
|
||||
using (var g = Graphics.FromImage(bmp))
|
||||
{
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.Clear(Color.Transparent);
|
||||
using (var bg = new SolidBrush(Color.FromArgb(46, 34, 28)))
|
||||
g.FillEllipse(bg, 0, 0, 31, 31);
|
||||
using (var gold = new SolidBrush(Cfg.Gold))
|
||||
g.FillEllipse(gold, 7, 7, 18, 18);
|
||||
using (var dark = new Pen(Cfg.GoldDark, 2.4f))
|
||||
g.DrawEllipse(dark, 7, 7, 18, 18);
|
||||
using (var hi = new SolidBrush(Color.FromArgb(230, 255, 250, 220)))
|
||||
g.FillEllipse(hi, 11, 10, 5, 4);
|
||||
}
|
||||
return Icon.FromHandle(bmp.GetHicon());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
namespace TempleRun;
|
||||
|
||||
/// <summary>
|
||||
/// 全局常量与可调参数。想改手感直接改这里。
|
||||
/// 世界坐标约定:X 向右为正,Y 离地向上为正,Z 玩家前方为正。
|
||||
/// 玩家永远停在 Z = 0,世界向 Z 负方向滚动。
|
||||
/// </summary>
|
||||
public static class Cfg
|
||||
{
|
||||
// ---- 相机(透视投影参数)----
|
||||
public const float Focal = 520f; // 焦距:越大视野越窄、远处物体越大
|
||||
public const float CamZ = 235f; // 相机位于玩家后方多远
|
||||
public const float CamHeight = 142f; // 相机离地高度
|
||||
public const float FarZ = 3000f; // 渲染最远距离
|
||||
public const float MinDepth = 45f; // 相机空间最小深度,防止除零
|
||||
|
||||
// ---- 跑道 ----
|
||||
public const int LaneCount = 3;
|
||||
public const float LaneWidth = 112f; // 单条车道宽
|
||||
public const float RoadHalfWidth = LaneWidth * 1.5f;
|
||||
|
||||
// ---- 玩家 ----
|
||||
public const float PlayerHeight = 62f; // 站立高度
|
||||
public const float SlideHeight = 30f; // 滑铲高度
|
||||
public const float PlayerHalfDepth = 22f;
|
||||
public const float JumpSpeed = 430f;
|
||||
public const float Gravity = 950f;
|
||||
public const float LaneLerp = 13f; // 换道插值速度
|
||||
public const float SlideDuration = 0.55f;
|
||||
|
||||
// ---- 速度与难度 ----
|
||||
public const float StartSpeed = 620f;
|
||||
public const float MaxSpeed = 1900f;
|
||||
public const float SpeedAccel = 3.4f; // 每秒自然加速
|
||||
public const float SpeedPerCoin = 5f; // 每金币额外加速
|
||||
public const float SpawnIntervalMin = 0.62f; // 相邻障碍组最小时间间隔(秒)
|
||||
public const float SpawnIntervalMax = 1.15f;
|
||||
|
||||
// ---- 颜色(丛林日落 + 古神庙)----
|
||||
public static readonly Color SkyTop = Color.FromArgb(30, 34, 72);
|
||||
public static readonly Color SkyMid = Color.FromArgb(122, 78, 96);
|
||||
public static readonly Color SkyHorizon = Color.FromArgb(240, 168, 96);
|
||||
public static readonly Color Fog = Color.FromArgb(200, 152, 118);
|
||||
public static readonly Color GroundNear = Color.FromArgb(44, 36, 28);
|
||||
public static readonly Color GroundFar = Color.FromArgb(122, 100, 78);
|
||||
public static readonly Color RoadA = Color.FromArgb(178, 150, 110);
|
||||
public static readonly Color RoadB = Color.FromArgb(156, 130, 94);
|
||||
public static readonly Color Stone = Color.FromArgb(120, 112, 104);
|
||||
public static readonly Color Wood = Color.FromArgb(122, 82, 48);
|
||||
public static readonly Color Vine = Color.FromArgb(74, 116, 62);
|
||||
public static readonly Color Gold = Color.FromArgb(255, 202, 62);
|
||||
public static readonly Color GoldDark = Color.FromArgb(196, 140, 24);
|
||||
public static readonly Color Player = Color.FromArgb(226, 92, 62);
|
||||
|
||||
public static float LaneX(int lane) => (lane - 1) * LaneWidth;
|
||||
|
||||
public static Color Lerp(Color a, Color b, float t)
|
||||
{
|
||||
t = Math.Clamp(t, 0f, 1f);
|
||||
return Color.FromArgb(
|
||||
(int)(a.R + (b.R - a.R) * t),
|
||||
(int)(a.G + (b.G - a.G) * t),
|
||||
(int)(a.B + (b.B - a.B) * t));
|
||||
}
|
||||
}
|
||||
|
||||
public enum RunState { Ready, Playing, Paused, GameOver }
|
||||
|
||||
public enum ObstacleKind
|
||||
{
|
||||
/// <summary>矮障碍:必须跳跃</summary>
|
||||
Low,
|
||||
/// <summary>悬空横梁:必须滑铲</summary>
|
||||
High,
|
||||
/// <summary>整道石墙:必须换道</summary>
|
||||
Full
|
||||
}
|
||||
|
||||
/// <summary>透视相机:把世界坐标投影到屏幕坐标。</summary>
|
||||
public static class Cam
|
||||
{
|
||||
public static float CenterX;
|
||||
public static float HorizonY;
|
||||
public static int Width;
|
||||
public static int Height;
|
||||
|
||||
public static void Setup(int w, int h)
|
||||
{
|
||||
Width = w;
|
||||
Height = h;
|
||||
CenterX = w * 0.5f;
|
||||
HorizonY = h * 0.30f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 投影一个世界点。
|
||||
/// scale 为"每世界单位对应多少像素",用于缩放实体自身尺寸。
|
||||
/// </summary>
|
||||
public static void Project(float x, float y, float z, out float sx, out float sy, out float scale)
|
||||
{
|
||||
float depth = z + Cfg.CamZ;
|
||||
if (depth < Cfg.MinDepth) depth = Cfg.MinDepth;
|
||||
scale = Cfg.Focal / depth;
|
||||
sx = CenterX + x * scale;
|
||||
sy = HorizonY + (Cfg.CamHeight - y) * scale;
|
||||
}
|
||||
|
||||
/// <summary>只投影地面上的点(y = 0)。</summary>
|
||||
public static void ProjectGround(float x, float z, out float sx, out float sy, out float scale)
|
||||
=> Project(x, 0f, z, out sx, out sy, out scale);
|
||||
|
||||
/// <summary>距离雾化系数:0 = 完全清晰,1 = 完全融入雾色。</summary>
|
||||
public static float FogAmount(float z)
|
||||
{
|
||||
float t = Math.Clamp(z / Cfg.FarZ, 0f, 1f);
|
||||
return t * t; // 平方衰减,近处保持通透
|
||||
}
|
||||
}
|
||||
|
||||
public class Obstacle
|
||||
{
|
||||
public float Z;
|
||||
public int Lane;
|
||||
public ObstacleKind Kind;
|
||||
public bool Scored; // 是否已计入"躲避成功"
|
||||
|
||||
public float Width => Kind == ObstacleKind.Full ? 96f : 90f;
|
||||
public float Depth => Kind == ObstacleKind.Full ? 54f : 56f;
|
||||
public float BottomY => Kind == ObstacleKind.High ? 40f : 0f;
|
||||
public float TopY => Kind switch
|
||||
{
|
||||
ObstacleKind.Low => 46f, // 低于跳跃高度,跳过去
|
||||
ObstacleKind.High => 98f, // 悬空横梁,滑铲钻过去
|
||||
_ => 108f // 整道石墙,只能换道
|
||||
};
|
||||
public float CenterX => Cfg.LaneX(Lane);
|
||||
}
|
||||
|
||||
public class Coin
|
||||
{
|
||||
public float Z;
|
||||
public int Lane;
|
||||
public float Y = 36f;
|
||||
public bool Taken;
|
||||
public float Phase;
|
||||
public float CenterX => Cfg.LaneX(Lane);
|
||||
}
|
||||
|
||||
public class Particle
|
||||
{
|
||||
public float X, Y, Z;
|
||||
public float Vx, Vy, Vz;
|
||||
public float Life;
|
||||
public float MaxLife;
|
||||
public float Size;
|
||||
public float GravityScale = 1f;
|
||||
public Color Color;
|
||||
}
|
||||
|
||||
/// <summary>路边装饰物:石柱、火把、树、雕像碎块。</summary>
|
||||
public class SideProp
|
||||
{
|
||||
public float Z;
|
||||
public int Side; // -1 左,+1 右
|
||||
public int Type; // 0=石柱 1=火把 2=枯树 3=碎石
|
||||
public float Height;
|
||||
public float Phase;
|
||||
|
||||
public float CenterX => Side * (Cfg.RoadHalfWidth + 205f + (Type == 3 ? 40f : 0f));
|
||||
}
|
||||
|
||||
public class Player
|
||||
{
|
||||
public float X; // 当前横向世界坐标(插值中)
|
||||
public int TargetLane = 1; // 目标车道 0/1/2
|
||||
public float Y; // 离地高度
|
||||
public float Vy;
|
||||
public bool OnGround = true;
|
||||
public bool Sliding;
|
||||
public float SlideTimer;
|
||||
public float RunPhase; // 跑动动画相位
|
||||
public bool Alive = true;
|
||||
public float Tilt; // 换道时的侧倾(视觉)
|
||||
|
||||
public float Height => Sliding ? Cfg.SlideHeight : Cfg.PlayerHeight;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
TargetLane = 1;
|
||||
X = Cfg.LaneX(1);
|
||||
Y = 0;
|
||||
Vy = 0;
|
||||
OnGround = true;
|
||||
Sliding = false;
|
||||
SlideTimer = 0;
|
||||
RunPhase = 0;
|
||||
Alive = true;
|
||||
Tilt = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Text;
|
||||
|
||||
namespace TempleRun;
|
||||
|
||||
/// <summary>
|
||||
/// 全部绘制逻辑。使用 GDI+ 手工做透视投影,不依赖任何 3D 库。
|
||||
/// </summary>
|
||||
public static class GameRenderer
|
||||
{
|
||||
// ---------------------------------------------------------------- 缓存对象
|
||||
private static Font _fHuge, _fTitle, _fBig, _fMid, _fSmall, _fTiny;
|
||||
private static readonly List<(float Z, int Type, object Obj)> _drawList = new(512);
|
||||
private const int TypeObstacle = 0;
|
||||
private const int TypeCoin = 1;
|
||||
|
||||
private static void EnsureFonts()
|
||||
{
|
||||
if (_fBig != null) return;
|
||||
const string face = "Microsoft YaHei UI";
|
||||
_fHuge = new Font(face, 46f, FontStyle.Bold, GraphicsUnit.Pixel);
|
||||
_fTitle = new Font(face, 30f, FontStyle.Bold, GraphicsUnit.Pixel);
|
||||
_fBig = new Font(face, 26f, FontStyle.Bold, GraphicsUnit.Pixel);
|
||||
_fMid = new Font(face, 19f, FontStyle.Regular, GraphicsUnit.Pixel);
|
||||
_fSmall = new Font(face, 15f, FontStyle.Regular, GraphicsUnit.Pixel);
|
||||
_fTiny = new Font(face, 12f, FontStyle.Regular, GraphicsUnit.Pixel);
|
||||
}
|
||||
|
||||
public static void ResetCache() { _fBig = null; }
|
||||
|
||||
// ---------------------------------------------------------------- 入口
|
||||
public static void Draw(Graphics g, int w, int h, GameWorld world, bool showFps = false)
|
||||
{
|
||||
if (w <= 0 || h <= 0) return;
|
||||
EnsureFonts();
|
||||
|
||||
Cam.Setup(w, h);
|
||||
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
|
||||
|
||||
g.Clear(Color.FromArgb(12, 12, 20));
|
||||
|
||||
// 屏幕震动
|
||||
float shakeX = 0f, shakeY = 0f;
|
||||
if (world.Shake > 0.15f)
|
||||
{
|
||||
float s = world.Shake;
|
||||
shakeX = (float)(Random.Shared.NextDouble() * 2 - 1) * s;
|
||||
shakeY = (float)(Random.Shared.NextDouble() * 2 - 1) * s * 0.7f;
|
||||
}
|
||||
|
||||
GraphicsState st = g.Save();
|
||||
g.TranslateTransform(shakeX, shakeY);
|
||||
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
DrawSky(g, w, h);
|
||||
DrawDistantScenery(g, w, h);
|
||||
DrawGround(g, w, h, world);
|
||||
DrawProps(g, world);
|
||||
DrawEntities(g, world);
|
||||
DrawParticles(g, world);
|
||||
DrawPlayer(g, world);
|
||||
|
||||
g.Restore(st);
|
||||
|
||||
// 撞击红闪
|
||||
if (world.HitFlash > 0.01f)
|
||||
{
|
||||
using var fb = new SolidBrush(Color.FromArgb((int)(130 * world.HitFlash), 220, 40, 30));
|
||||
g.FillRectangle(fb, 0, 0, w, h);
|
||||
}
|
||||
|
||||
DrawHud(g, w, h, world, showFps);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 投影辅助
|
||||
private static PointF P(float x, float y, float z)
|
||||
{
|
||||
Cam.Project(x, y, z, out float sx, out float sy, out _);
|
||||
return new PointF(sx, sy);
|
||||
}
|
||||
|
||||
private static float PX(float x, float y, float z)
|
||||
{
|
||||
Cam.Project(x, y, z, out float sx, out _, out _);
|
||||
return sx;
|
||||
}
|
||||
|
||||
private static float PY(float x, float y, float z)
|
||||
{
|
||||
Cam.Project(x, y, z, out _, out float sy, out _);
|
||||
return sy;
|
||||
}
|
||||
|
||||
private static float PS(float x, float y, float z)
|
||||
{
|
||||
Cam.Project(x, y, z, out _, out _, out float s);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 天空
|
||||
private static void DrawSky(Graphics g, int w, int h)
|
||||
{
|
||||
int skyBottom = (int)Cam.HorizonY + 30;
|
||||
var rect = new Rectangle(0, -30, w, skyBottom + 30);
|
||||
if (rect.Height < 2) return;
|
||||
|
||||
using var br = new LinearGradientBrush(rect, Color.Black, Color.White, 90f)
|
||||
{
|
||||
InterpolationColors = new ColorBlend(3)
|
||||
{
|
||||
Colors = new[] { Cfg.SkyTop, Cfg.SkyMid, Cfg.SkyHorizon },
|
||||
Positions = new[] { 0f, 0.60f, 1f }
|
||||
}
|
||||
};
|
||||
g.FillRectangle(br, rect);
|
||||
|
||||
// 太阳
|
||||
float sunX = w * 0.74f;
|
||||
float sunY = Cam.HorizonY - h * 0.115f;
|
||||
float sunR = Math.Max(16f, h * 0.072f);
|
||||
|
||||
using (var path = new GraphicsPath())
|
||||
{
|
||||
path.AddEllipse(sunX - sunR * 3.4f, sunY - sunR * 3.4f, sunR * 6.8f, sunR * 6.8f);
|
||||
using var glow = new PathGradientBrush(path)
|
||||
{
|
||||
CenterColor = Color.FromArgb(140, 255, 220, 150),
|
||||
SurroundColors = new[] { Color.FromArgb(0, 255, 200, 120) }
|
||||
};
|
||||
g.FillPath(glow, path);
|
||||
}
|
||||
using (var sb = new SolidBrush(Color.FromArgb(255, 255, 240, 200)))
|
||||
g.FillEllipse(sb, sunX - sunR, sunY - sunR, sunR * 2, sunR * 2);
|
||||
|
||||
// 云条
|
||||
using (var cb = new SolidBrush(Color.FromArgb(46, 255, 226, 190)))
|
||||
{
|
||||
g.FillEllipse(cb, w * 0.10f, Cam.HorizonY - h * 0.20f, w * 0.16f, h * 0.022f);
|
||||
g.FillEllipse(cb, w * 0.30f, Cam.HorizonY - h * 0.255f, w * 0.11f, h * 0.017f);
|
||||
g.FillEllipse(cb, w * 0.62f, Cam.HorizonY - h * 0.16f, w * 0.20f, h * 0.020f);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 远景(山脊 + 神庙)
|
||||
private static void DrawDistantScenery(Graphics g, int w, int h)
|
||||
{
|
||||
float hy = Cam.HorizonY;
|
||||
|
||||
// 远山脊
|
||||
using (var far = new SolidBrush(Cfg.Lerp(Cfg.Fog, Color.FromArgb(72, 62, 104), 0.42f)))
|
||||
{
|
||||
using var path = new GraphicsPath();
|
||||
path.AddPolygon(new[]
|
||||
{
|
||||
new PointF(-w * 0.05f, hy + 2),
|
||||
new PointF(w * 0.08f, hy - h * 0.075f),
|
||||
new PointF(w * 0.20f, hy - h * 0.012f),
|
||||
new PointF(w * 0.33f, hy - h * 0.105f),
|
||||
new PointF(w * 0.47f, hy - h * 0.020f),
|
||||
new PointF(w * 0.60f, hy - h * 0.088f),
|
||||
new PointF(w * 0.74f, hy - h * 0.028f),
|
||||
new PointF(w * 0.88f, hy - h * 0.070f),
|
||||
new PointF(w * 1.05f, hy + 2)
|
||||
});
|
||||
g.FillPath(far, path);
|
||||
}
|
||||
|
||||
// 神庙剪影(左)
|
||||
DrawTemple(g, w * 0.19f, hy + 4, w * 0.115f, h * 0.155f,
|
||||
Cfg.Lerp(Cfg.Fog, Color.FromArgb(58, 48, 78), 0.55f), 5);
|
||||
|
||||
// 神庙剪影(右,稍小)
|
||||
DrawTemple(g, w * 0.83f, hy + 4, w * 0.085f, h * 0.115f,
|
||||
Cfg.Lerp(Cfg.Fog, Color.FromArgb(66, 56, 86), 0.46f), 4);
|
||||
|
||||
// 地平线雾带
|
||||
using var haze = new LinearGradientBrush(
|
||||
new Rectangle(0, (int)hy - 26, w, 58),
|
||||
Color.FromArgb(0, Cfg.Fog), Color.FromArgb(190, Cfg.Fog), 90f);
|
||||
g.FillRectangle(haze, 0, hy - 26, w, 58);
|
||||
}
|
||||
|
||||
private static void DrawTemple(Graphics g, float cx, float baseY, float w, float h, Color c, int steps)
|
||||
{
|
||||
using var br = new SolidBrush(c);
|
||||
float sh = h / steps;
|
||||
for (int i = 0; i < steps; i++)
|
||||
{
|
||||
float frac = 1f - (float)i / steps * 0.70f;
|
||||
float ww = w * frac;
|
||||
float y = baseY - sh * (i + 1);
|
||||
g.FillRectangle(br, cx - ww * 0.5f, y, ww, sh + 1.2f);
|
||||
}
|
||||
// 塔尖
|
||||
float topW = w * 0.30f;
|
||||
float topY = baseY - h;
|
||||
g.FillPolygon(br, new[]
|
||||
{
|
||||
new PointF(cx - topW * 0.5f, topY + 1),
|
||||
new PointF(cx, topY - sh * 0.85f),
|
||||
new PointF(cx + topW * 0.5f, topY + 1)
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 地面与跑道
|
||||
private static void DrawGround(Graphics g, int w, int h, GameWorld world)
|
||||
{
|
||||
// 整体地面渐变
|
||||
int gy = (int)Cam.HorizonY;
|
||||
var rect = new Rectangle(0, gy - 2, w, h - gy + 4);
|
||||
if (rect.Height > 1)
|
||||
{
|
||||
using var gb = new LinearGradientBrush(rect, Color.White, Color.White, 90f)
|
||||
{
|
||||
InterpolationColors = new ColorBlend(4)
|
||||
{
|
||||
Colors = new[] { Cfg.GroundFar, Cfg.Lerp(Cfg.GroundFar, Cfg.GroundNear, 0.35f), Cfg.GroundNear, Color.FromArgb(52, 40, 30) },
|
||||
Positions = new[] { 0f, 0.18f, 0.62f, 1f }
|
||||
}
|
||||
};
|
||||
g.FillRectangle(gb, rect);
|
||||
}
|
||||
|
||||
SmoothingMode old = g.SmoothingMode;
|
||||
g.SmoothingMode = SmoothingMode.None; // 条纹用无抗锯齿更快
|
||||
|
||||
const float period = 190f;
|
||||
const float drawNear = -230f;
|
||||
|
||||
// 跑道两侧的暗色沟渠
|
||||
DrawSideBand(g, w, -1);
|
||||
DrawSideBand(g, w, +1);
|
||||
|
||||
// 跑道条纹(世界坐标固定,随世界滚动)
|
||||
float offset = world.Distance % (period * 2f);
|
||||
for (int i = -8; i < 40; i++)
|
||||
{
|
||||
float z0 = i * period * 2f - offset;
|
||||
float z1 = z0 + period;
|
||||
if (z1 < drawNear) continue;
|
||||
if (z0 > Cfg.FarZ) break;
|
||||
|
||||
z0 = Math.Max(z0, drawNear);
|
||||
z1 = Math.Min(z1, Cfg.FarZ);
|
||||
if (z1 <= z0) continue;
|
||||
|
||||
float fog = Cam.FogAmount(z0);
|
||||
Color baseColor = ((i % 2) + 2) % 2 == 0 ? Cfg.RoadA : Cfg.RoadB;
|
||||
Color c = Cfg.Lerp(baseColor, Cfg.Fog, fog * 0.92f);
|
||||
|
||||
float lx0 = PX(-Cfg.RoadHalfWidth, 0, z0);
|
||||
float rx0 = PX(+Cfg.RoadHalfWidth, 0, z0);
|
||||
float ly0 = PY(0, 0, z0);
|
||||
float ly1 = PY(0, 0, z1);
|
||||
float lx1 = PX(-Cfg.RoadHalfWidth, 0, z1);
|
||||
float rx1 = PX(+Cfg.RoadHalfWidth, 0, z1);
|
||||
|
||||
using var br = new SolidBrush(c);
|
||||
g.FillPolygon(br, new[]
|
||||
{
|
||||
new PointF(lx0, ly0), new PointF(rx0, ly0),
|
||||
new PointF(rx1, ly1), new PointF(lx1, ly1)
|
||||
});
|
||||
}
|
||||
|
||||
// 跑道边缘描边
|
||||
DrawRoadEdge(g, -1);
|
||||
DrawRoadEdge(g, +1);
|
||||
|
||||
// 车道分隔虚线
|
||||
DrawLaneMark(g, -Cfg.LaneWidth * 0.5f, world, period);
|
||||
DrawLaneMark(g, +Cfg.LaneWidth * 0.5f, world, period);
|
||||
|
||||
g.SmoothingMode = old;
|
||||
}
|
||||
|
||||
private static void DrawRoadEdge(Graphics g, int side)
|
||||
{
|
||||
float x = side * Cfg.RoadHalfWidth;
|
||||
var pts = new List<PointF>();
|
||||
for (float z = Cfg.FarZ; z >= -230f; z -= 120f)
|
||||
pts.Add(new PointF(PX(x, 0, z), PY(0, 0, z)));
|
||||
|
||||
if (pts.Count < 2) return;
|
||||
using var pen = new Pen(Color.FromArgb(120, 58, 44, 32), 3.2f);
|
||||
g.DrawLines(pen, pts.ToArray());
|
||||
}
|
||||
|
||||
private static void DrawSideBand(Graphics g, int w, int side)
|
||||
{
|
||||
float xi = side * Cfg.RoadHalfWidth;
|
||||
float xo = side * (Cfg.RoadHalfWidth + 170f);
|
||||
|
||||
float yFar = PY(0, 0, Cfg.FarZ);
|
||||
float yNear = PY(0, 0, -230f);
|
||||
var poly = new[]
|
||||
{
|
||||
new PointF(PX(xi, 0, Cfg.FarZ), yFar), new PointF(PX(xo, 0, Cfg.FarZ), yFar),
|
||||
new PointF(PX(xo, 0, -230f), yNear), new PointF(PX(xi, 0, -230f), yNear)
|
||||
};
|
||||
using var br = new SolidBrush(Color.FromArgb(96, 34, 26, 20));
|
||||
g.FillPolygon(br, poly);
|
||||
}
|
||||
|
||||
private static void DrawLaneMark(Graphics g, float x, GameWorld world, float period)
|
||||
{
|
||||
float offset = world.Distance % (period * 2f);
|
||||
for (int i = -8; i < 40; i++)
|
||||
{
|
||||
float z0 = i * period * 2f - offset;
|
||||
float z1 = z0 + period * 0.45f;
|
||||
if (z1 < -230f) continue;
|
||||
if (z0 > Cfg.FarZ) break;
|
||||
z0 = Math.Max(z0, -230f);
|
||||
z1 = Math.Min(z1, Cfg.FarZ);
|
||||
if (z1 <= z0) continue;
|
||||
|
||||
float fog = Cam.FogAmount(z0);
|
||||
var c = Cfg.Lerp(Color.FromArgb(168, 146, 108), Cfg.Fog, fog * 0.9f);
|
||||
using var br = new SolidBrush(Color.FromArgb((byte)(120 * (1 - fog) + 30), c));
|
||||
g.FillPolygon(br, new[]
|
||||
{
|
||||
new PointF(PX(x - 4, 0, z0), PY(0, 0, z0)), new PointF(PX(x + 4, 0, z0), PY(0, 0, z0)),
|
||||
new PointF(PX(x + 4, 0, z1), PY(0, 0, z1)), new PointF(PX(x - 4, 0, z1), PY(0, 0, z1))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 路边装饰
|
||||
private static void DrawProps(Graphics g, GameWorld world)
|
||||
{
|
||||
var props = world.Props;
|
||||
for (int i = 0; i < props.Count; i++)
|
||||
{
|
||||
var p = props[i];
|
||||
if (p.Z < 70f) continue; // 太近会盖住跑道,直接跳过
|
||||
|
||||
float fog = Cam.FogAmount(p.Z);
|
||||
if (fog > 0.96f) continue;
|
||||
|
||||
float wx = p.CenterX; // 世界坐标
|
||||
float sx = PX(wx, 0f, p.Z); // ★ 屏幕 x(原来误用了世界坐标)
|
||||
float top = PY(wx, p.Height, p.Z);
|
||||
float bottom = PY(wx, 0f, p.Z);
|
||||
float s = PS(wx, 0f, p.Z);
|
||||
float wpx = Math.Min(40f * s, 220f); // 限制近处被夸张放大的尺寸
|
||||
if (bottom < Cam.HorizonY - 60) continue;
|
||||
|
||||
switch (p.Type)
|
||||
{
|
||||
case 0: // 石柱
|
||||
{
|
||||
var body = Cfg.Lerp(Color.FromArgb(96, 86, 76), Cfg.Fog, fog);
|
||||
using var br = new SolidBrush(body);
|
||||
g.FillRectangle(br, sx - wpx * 0.5f, top, wpx, bottom - top);
|
||||
using var cap = new SolidBrush(Cfg.Lerp(Color.FromArgb(124, 110, 96), Cfg.Fog, fog));
|
||||
g.FillRectangle(cap, sx - wpx * 0.62f, top - wpx * 0.22f, wpx * 1.24f, wpx * 0.26f);
|
||||
using var pen = new Pen(Color.FromArgb(80, 34, 28, 22), Math.Max(1f, s * 1.4f));
|
||||
g.DrawLine(pen, sx - wpx * 0.18f, top, sx - wpx * 0.18f, bottom);
|
||||
break;
|
||||
}
|
||||
case 1: // 火把
|
||||
{
|
||||
using var br = new SolidBrush(Cfg.Lerp(Color.FromArgb(88, 60, 38), Cfg.Fog, fog));
|
||||
g.FillRectangle(br, sx - wpx * 0.16f, top, wpx * 0.32f, bottom - top);
|
||||
float fr = wpx * 0.52f * (1f + 0.14f * MathF.Sin(world.Clock * 6f + p.Phase));
|
||||
using var glow = new SolidBrush(Color.FromArgb((int)(130 * (1 - fog)), 255, 170, 62));
|
||||
g.FillEllipse(glow, sx - fr, top - fr * 1.1f, fr * 2, fr * 2);
|
||||
using var core = new SolidBrush(Color.FromArgb((int)(235 * (1 - fog)), 255, 234, 158));
|
||||
g.FillEllipse(core, sx - fr * 0.42f, top - fr * 0.95f, fr * 0.84f, fr * 0.95f);
|
||||
break;
|
||||
}
|
||||
case 2: // 枯树
|
||||
{
|
||||
var wood = Cfg.Lerp(Color.FromArgb(64, 50, 38), Cfg.Fog, fog);
|
||||
using var br = new SolidBrush(wood);
|
||||
float trunkW = Math.Max(2f, wpx * 0.20f);
|
||||
g.FillRectangle(br, sx - trunkW * 0.5f, top, trunkW, bottom - top);
|
||||
using var pen = new Pen(wood, Math.Max(1.6f, wpx * 0.11f))
|
||||
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
float midY = top + (bottom - top) * 0.42f;
|
||||
g.DrawLine(pen, sx, midY, sx - wpx * 0.85f, midY - (bottom - top) * 0.30f);
|
||||
g.DrawLine(pen, sx, midY + (bottom - top) * 0.12f, sx + wpx * 0.90f, midY - (bottom - top) * 0.20f);
|
||||
break;
|
||||
}
|
||||
default: // 碎石
|
||||
{
|
||||
using var br = new SolidBrush(Cfg.Lerp(Color.FromArgb(84, 76, 68), Cfg.Fog, fog));
|
||||
float hgt = (bottom - top) * 1.1f;
|
||||
g.FillEllipse(br, sx - wpx * 0.7f, bottom - hgt, wpx * 1.4f, hgt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 实体(障碍 + 金币)
|
||||
private static void DrawEntities(Graphics g, GameWorld world)
|
||||
{
|
||||
_drawList.Clear();
|
||||
for (int i = 0; i < world.Obstacles.Count; i++)
|
||||
{
|
||||
var o = world.Obstacles[i];
|
||||
if (o.Z > Cfg.FarZ + 200f || o.Z < -260f) continue;
|
||||
_drawList.Add((o.Z, TypeObstacle, o));
|
||||
}
|
||||
for (int i = 0; i < world.Coins.Count; i++)
|
||||
{
|
||||
var c = world.Coins[i];
|
||||
if (c.Taken || c.Z > Cfg.FarZ + 200f || c.Z < -260f) continue;
|
||||
_drawList.Add((c.Z, TypeCoin, c));
|
||||
}
|
||||
|
||||
_drawList.Sort(static (a, b) => b.Z.CompareTo(a.Z)); // 远 → 近
|
||||
|
||||
for (int i = 0; i < _drawList.Count; i++)
|
||||
{
|
||||
var it = _drawList[i];
|
||||
if (it.Type == TypeObstacle) DrawObstacle(g, (Obstacle)it.Obj);
|
||||
else DrawCoin(g, (Coin)it.Obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawObstacle(Graphics g, Obstacle o)
|
||||
{
|
||||
float fog = Cam.FogAmount(o.Z);
|
||||
if (fog > 0.97f) return;
|
||||
|
||||
float cx = o.CenterX;
|
||||
float hw = o.Width * 0.5f;
|
||||
float zn = o.Z - o.Depth * 0.5f;
|
||||
float zf = o.Z + o.Depth * 0.5f;
|
||||
float yb = o.BottomY, yt = o.TopY;
|
||||
|
||||
// 8 个顶点
|
||||
PointF nlb = P(cx - hw, yb, zn), nrb = P(cx + hw, yb, zn);
|
||||
PointF nlt = P(cx - hw, yt, zn), nrt = P(cx + hw, yt, zn);
|
||||
PointF flb = P(cx - hw, yb, zf), frb = P(cx + hw, yb, zf);
|
||||
PointF flt = P(cx - hw, yt, zf), frt = P(cx + hw, yt, zf);
|
||||
|
||||
// 落地阴影(让障碍"坐"在跑道上)
|
||||
if (o.BottomY <= 1f)
|
||||
{
|
||||
Cam.Project(cx, 0.6f, o.Z, out float shx, out float shy, out float shs);
|
||||
using var shb = new SolidBrush(Color.FromArgb(78, 0, 0, 0));
|
||||
g.FillEllipse(shb, shx - o.Width * 0.62f * shs, shy - 8f * shs,
|
||||
o.Width * 1.24f * shs, 16f * shs);
|
||||
}
|
||||
|
||||
Color cFront, cTop, cSide;
|
||||
switch (o.Kind)
|
||||
{
|
||||
case ObstacleKind.Low:
|
||||
cFront = Color.FromArgb(78, 70, 62);
|
||||
cTop = Color.FromArgb(120, 108, 94);
|
||||
cSide = Color.FromArgb(56, 50, 44);
|
||||
break;
|
||||
case ObstacleKind.High:
|
||||
cFront = Color.FromArgb(112, 72, 40);
|
||||
cTop = Color.FromArgb(150, 106, 58);
|
||||
cSide = Color.FromArgb(80, 52, 28);
|
||||
break;
|
||||
default:
|
||||
cFront = Color.FromArgb(64, 60, 58);
|
||||
cTop = Color.FromArgb(104, 96, 90);
|
||||
cSide = Color.FromArgb(46, 44, 42);
|
||||
break;
|
||||
}
|
||||
|
||||
cFront = Cfg.Lerp(cFront, Cfg.Fog, fog);
|
||||
cTop = Cfg.Lerp(cTop, Cfg.Fog, fog);
|
||||
cSide = Cfg.Lerp(cSide, Cfg.Fog, fog);
|
||||
|
||||
// 顶面
|
||||
using (var br = new SolidBrush(cTop))
|
||||
g.FillPolygon(br, new[] { nlt, nrt, frt, flt });
|
||||
|
||||
// 侧面(朝向画面中心的那一面)
|
||||
bool showLeft = cx > 0f;
|
||||
using (var br = new SolidBrush(cSide))
|
||||
{
|
||||
var quad = showLeft
|
||||
? new[] { nlb, nlt, flt, flb }
|
||||
: new[] { nrb, nrt, frt, frb };
|
||||
g.FillPolygon(br, quad);
|
||||
}
|
||||
|
||||
// 近面
|
||||
using (var br = new SolidBrush(cFront))
|
||||
g.FillPolygon(br, new[] { nlb, nrb, nrt, nlt });
|
||||
|
||||
// 近面描边 + 细节
|
||||
float lw = Math.Max(1f, PS(cx, 0, o.Z) * 1.6f);
|
||||
using (var pen = new Pen(Color.FromArgb(150, 34, 26, 20), lw))
|
||||
g.DrawPolygon(pen, new[] { nlb, nrb, nrt, nlt });
|
||||
|
||||
float s = PS(cx, 0, o.Z);
|
||||
if (s > 0.35f)
|
||||
{
|
||||
if (o.Kind == ObstacleKind.High)
|
||||
{
|
||||
// 横梁上的藤蔓
|
||||
using var vine = new Pen(Cfg.Lerp(Cfg.Vine, Cfg.Fog, fog), Math.Max(1.5f, s * 2.4f))
|
||||
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
float topY = (nlt.Y + nrt.Y) * 0.5f;
|
||||
float botY = (nlb.Y + nrb.Y) * 0.5f;
|
||||
for (int k = 0; k < 3; k++)
|
||||
{
|
||||
float fx = nlb.X + (nrb.X - nlb.X) * (0.22f + k * 0.28f);
|
||||
g.DrawLine(vine, fx, topY + (botY - topY) * 0.30f, fx + s * 6f, botY + s * 10f);
|
||||
}
|
||||
}
|
||||
else if (o.Kind == ObstacleKind.Full)
|
||||
{
|
||||
// 石墙刻纹
|
||||
using var pen = new Pen(Color.FromArgb(90, 40, 34, 30), Math.Max(1f, s * 1.8f));
|
||||
for (int k = 1; k <= 2; k++)
|
||||
{
|
||||
float t = k / 3f;
|
||||
float y = nlb.Y + (nlt.Y - nlb.Y) * t;
|
||||
g.DrawLine(pen, nlb.X + s * 4f, y, nrb.X - s * 4f, y);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 矮石堆的碎石纹理
|
||||
using var br2 = new SolidBrush(Color.FromArgb(70, 46, 38, 30));
|
||||
g.FillEllipse(br2, nlb.X + (nrb.X - nlb.X) * 0.16f, nlb.Y - (nlb.Y - nlt.Y) * 0.55f,
|
||||
(nrb.X - nlb.X) * 0.26f, (nlb.Y - nlt.Y) * 0.30f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCoin(Graphics g, Coin c)
|
||||
{
|
||||
float fog = Cam.FogAmount(c.Z);
|
||||
if (fog > 0.96f) return;
|
||||
|
||||
Cam.Project(c.CenterX, c.Y, c.Z, out float sx, out float sy, out float s);
|
||||
float r = Math.Min(10f * s, 46f);
|
||||
if (r < 0.6f) return;
|
||||
|
||||
float wobble = MathF.Abs(MathF.Cos(c.Phase));
|
||||
float rx = r * (0.30f + 0.70f * wobble);
|
||||
float ry = r;
|
||||
|
||||
int fade = (int)(255 * (1f - fog * 0.9f));
|
||||
Color gold = Cfg.Lerp(Cfg.Gold, Cfg.Fog, fog * 0.7f);
|
||||
Color goldDark = Cfg.Lerp(Cfg.GoldDark, Cfg.Fog, fog * 0.7f);
|
||||
|
||||
// 光晕
|
||||
using (var glow = new SolidBrush(Color.FromArgb(Math.Max(0, fade / 6), gold)))
|
||||
g.FillEllipse(glow, sx - ry * 1.4f, sy - ry * 1.4f, ry * 2.8f, ry * 2.8f);
|
||||
|
||||
using (var br = new SolidBrush(Color.FromArgb(fade, gold)))
|
||||
g.FillEllipse(br, sx - rx, sy - ry, rx * 2, ry * 2);
|
||||
|
||||
using (var pen = new Pen(Color.FromArgb(fade, goldDark), Math.Max(1f, r * 0.16f)))
|
||||
g.DrawEllipse(pen, sx - rx, sy - ry, rx * 2, ry * 2);
|
||||
|
||||
// 高光
|
||||
if (r > 3f)
|
||||
{
|
||||
using var hl = new SolidBrush(Color.FromArgb((int)(fade * 0.75f), 255, 250, 220));
|
||||
g.FillEllipse(hl, sx - rx * 0.42f, sy - ry * 0.52f, rx * 0.46f, ry * 0.40f);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 粒子
|
||||
private static void DrawParticles(Graphics g, GameWorld world)
|
||||
{
|
||||
var list = world.Particles;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
var p = list[i];
|
||||
float t = p.Life / p.MaxLife;
|
||||
int alpha = (int)(255 * (1f - t * t));
|
||||
if (alpha <= 4) continue;
|
||||
|
||||
Cam.Project(p.X, p.Y, p.Z, out float sx, out float sy, out float s);
|
||||
float r = Math.Max(0.8f, p.Size * s * (1f - t * 0.45f));
|
||||
|
||||
using var br = new SolidBrush(Color.FromArgb(alpha, p.Color));
|
||||
g.FillEllipse(br, sx - r, sy - r, r * 2, r * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 玩家
|
||||
private static void DrawPlayer(Graphics g, GameWorld world)
|
||||
{
|
||||
var h = world.Hero;
|
||||
|
||||
float footX = PX(h.X, h.Y, 0f);
|
||||
float footY = PY(h.X, h.Y, 0f);
|
||||
float headY = PY(h.X, h.Y + h.Height, 0f);
|
||||
float s = PS(h.X, 0f, 0f);
|
||||
|
||||
float bodyH = Math.Max(3f, footY - headY);
|
||||
|
||||
// ---- 地面阴影 ----
|
||||
float shX = PX(h.X, 0f, 0f);
|
||||
float shY = PY(h.X, 0f, 0f);
|
||||
float lift = Math.Clamp(h.Y / 220f, 0f, 1f);
|
||||
float shW = 58f * s * (1f - lift * 0.55f);
|
||||
float shH = 17f * s * (1f - lift * 0.50f);
|
||||
if (shW > 1f)
|
||||
{
|
||||
using var sb = new SolidBrush(Color.FromArgb((int)(120 * (1f - lift)), 0, 0, 0));
|
||||
g.FillEllipse(sb, shX - shW * 0.5f, shY - shH * 0.5f, shW, shH);
|
||||
}
|
||||
|
||||
if (world.State == RunState.GameOver && !h.Alive)
|
||||
{
|
||||
// 倒地:简单压扁 + 变灰
|
||||
DrawHero(g, footX, footY, bodyH, s, h, flattened: true);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawHero(g, footX, footY, bodyH, s, h, flattened: false);
|
||||
}
|
||||
|
||||
private static void DrawHero(Graphics g, float footX, float footY, float bodyH, float s,
|
||||
Player h, bool flattened)
|
||||
{
|
||||
// 影子偏移(换道侧倾)
|
||||
float lean = flattened ? 0f : h.Tilt;
|
||||
float cx = footX + lean * bodyH * 0.45f;
|
||||
|
||||
Color cloth = flattened ? Color.FromArgb(150, 110, 100, 96) : Cfg.Player;
|
||||
Color clothDark = flattened ? Color.FromArgb(150, 84, 74, 70) : Color.FromArgb(176, 62, 40);
|
||||
Color skin = flattened ? Color.FromArgb(150, 170, 150, 132) : Color.FromArgb(242, 196, 152);
|
||||
Color hair = Color.FromArgb(48, 36, 30);
|
||||
|
||||
float unit = bodyH / 62f; // 1 = 站立时的相对比例
|
||||
|
||||
using var penBody = new Pen(cloth, Math.Max(2.2f, bodyH * 0.155f))
|
||||
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
using var penLimb = new Pen(clothDark, Math.Max(2f, bodyH * 0.115f))
|
||||
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
|
||||
float hipY = footY - bodyH * 0.42f;
|
||||
float shoulderY = footY - bodyH * 0.76f;
|
||||
|
||||
if (flattened)
|
||||
{
|
||||
// 趴倒
|
||||
float len = bodyH * 1.05f;
|
||||
using var bodyPen = new Pen(cloth, Math.Max(2.4f, bodyH * 0.17f))
|
||||
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
g.DrawLine(bodyPen, cx - len * 0.32f, footY - bodyH * 0.16f, cx + len * 0.38f, footY - bodyH * 0.10f);
|
||||
using var headBr = new SolidBrush(skin);
|
||||
float hr = bodyH * 0.135f;
|
||||
g.FillEllipse(headBr, cx + len * 0.34f - hr, footY - bodyH * 0.26f - hr, hr * 2, hr * 2);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 腿 ----
|
||||
float swing = MathF.Sin(h.RunPhase);
|
||||
bool airborne = !h.OnGround;
|
||||
float legLen = bodyH * 0.42f;
|
||||
|
||||
void DrawLeg(float sign)
|
||||
{
|
||||
float lift = airborne ? 0.42f : MathF.Max(0f, sign * swing) * 0.75f;
|
||||
float fx = cx + sign * 9f * unit + (airborne ? 0f : sign * swing * 12f * unit);
|
||||
float fy = footY - legLen * (airborne ? 0.45f : lift);
|
||||
float kneeX = cx + sign * 6f * unit + (fx - cx) * 0.4f;
|
||||
float kneeY = (hipY + fy) * 0.5f;
|
||||
g.DrawLine(penLimb, cx + sign * 5f * unit, hipY, kneeX, kneeY);
|
||||
g.DrawLine(penLimb, kneeX, kneeY, fx, fy);
|
||||
}
|
||||
DrawLeg(-1f);
|
||||
DrawLeg(+1f);
|
||||
|
||||
// ---- 躯干 ----
|
||||
float torsoW = bodyH * 0.30f;
|
||||
using (var torso = new SolidBrush(cloth))
|
||||
{
|
||||
var rc = new RectangleF(cx - torsoW * 0.5f, shoulderY, torsoW, hipY - shoulderY);
|
||||
using var rp = RoundRect(rc, torsoW * 0.34f);
|
||||
g.FillPath(torso, rp);
|
||||
}
|
||||
|
||||
// ---- 手臂(与腿反向摆动) ----
|
||||
void DrawArm(float sign)
|
||||
{
|
||||
float a = -sign * swing;
|
||||
float ex = cx + sign * (bodyH * 0.20f) + a * bodyH * 0.15f;
|
||||
float ey = shoulderY + bodyH * 0.26f - MathF.Abs(a) * bodyH * 0.10f;
|
||||
float midX = cx + sign * (bodyH * 0.19f);
|
||||
float midY = shoulderY + bodyH * 0.13f;
|
||||
g.DrawLine(penLimb, cx + sign * bodyH * 0.13f, shoulderY + bodyH * 0.03f, midX, midY);
|
||||
g.DrawLine(penLimb, midX, midY, ex, ey);
|
||||
}
|
||||
DrawArm(-1f);
|
||||
DrawArm(+1f);
|
||||
|
||||
// ---- 头 ----
|
||||
float headR = bodyH * 0.145f;
|
||||
float headCY = shoulderY - headR * 0.95f;
|
||||
using (var hb = new SolidBrush(skin))
|
||||
g.FillEllipse(hb, cx - headR, headCY - headR, headR * 2, headR * 2);
|
||||
|
||||
// 头发(上半圆)
|
||||
using (var hb = new SolidBrush(hair))
|
||||
{
|
||||
var rc = new RectangleF(cx - headR * 1.02f, headCY - headR * 1.10f, headR * 2.04f, headR * 1.35f);
|
||||
using var rp = RoundRect(rc, headR * 0.75f);
|
||||
g.FillPath(hb, rp);
|
||||
}
|
||||
|
||||
// 滑铲时的速度线
|
||||
if (h.Sliding)
|
||||
{
|
||||
using var sl = new Pen(Color.FromArgb(130, 255, 240, 210), Math.Max(1.4f, bodyH * 0.05f))
|
||||
{ StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
float yy = footY - bodyH * (0.10f + i * 0.14f);
|
||||
g.DrawLine(sl, cx - bodyH * 0.42f, yy, cx - bodyH * 0.18f, yy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- HUD
|
||||
private static void DrawHud(Graphics g, int w, int h, GameWorld world, bool showFps)
|
||||
{
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
|
||||
// ---- 左上:分数 / 金币 ----
|
||||
DrawChip(g, 18, 14, 236, 92);
|
||||
|
||||
string score = world.Score.ToString();
|
||||
using (var sh = new SolidBrush(Color.FromArgb(130, 0, 0, 0)))
|
||||
g.DrawString(score, _fHuge, sh, 32 + 2, 16 + 2);
|
||||
using (var sb = new SolidBrush(Color.White))
|
||||
g.DrawString(score, _fHuge, sb, 32, 16);
|
||||
|
||||
// 金币行
|
||||
float coinY = 78;
|
||||
float pop = 1f + world.CoinPop * 0.35f;
|
||||
float cr = 9f * pop;
|
||||
using (var gb = new SolidBrush(Cfg.Gold))
|
||||
g.FillEllipse(gb, 34 - cr, coinY - cr, cr * 2, cr * 2);
|
||||
using (var gp = new Pen(Cfg.GoldDark, 2f))
|
||||
g.DrawEllipse(gp, 34 - cr, coinY - cr, cr * 2, cr * 2);
|
||||
using (var sb = new SolidBrush(Color.FromArgb(255, 245, 210)))
|
||||
g.DrawString("× " + world.CoinCount, _fMid, sb, 52, coinY - 13);
|
||||
|
||||
// ---- 右上:速度 / 最佳 ----
|
||||
string speedTxt = "速度 ×" + (world.Speed / Cfg.StartSpeed).ToString("0.00");
|
||||
SizeF st = g.MeasureString(speedTxt, _fMid);
|
||||
DrawChip(g, w - st.Width - 44, 16, st.Width + 26, 34);
|
||||
using (var sb = new SolidBrush(Color.FromArgb(255, 226, 170)))
|
||||
g.DrawString(speedTxt, _fMid, sb, w - st.Width - 31, 24);
|
||||
|
||||
string bestTxt = "最佳 " + world.Best;
|
||||
SizeF bt = g.MeasureString(bestTxt, _fSmall);
|
||||
using (var sb = new SolidBrush(Color.FromArgb(210, 236, 226, 214)))
|
||||
g.DrawString(bestTxt, _fSmall, sb, w - bt.Width - 22, 58);
|
||||
|
||||
if (world.Muted)
|
||||
{
|
||||
using var sb = new SolidBrush(Color.FromArgb(150, 236, 226, 214));
|
||||
g.DrawString("已静音 (M)", _fTiny, sb, w - 96, 82);
|
||||
}
|
||||
|
||||
// ---- 中间状态层 ----
|
||||
switch (world.State)
|
||||
{
|
||||
case RunState.Ready: DrawReady(g, w, h, world); break;
|
||||
case RunState.Paused: DrawPaused(g, w, h); break;
|
||||
case RunState.GameOver: DrawGameOver(g, w, h, world); break;
|
||||
}
|
||||
|
||||
// ---- 右下角操作提示 ----
|
||||
using (var sb = new SolidBrush(Color.FromArgb(120, 240, 234, 226)))
|
||||
g.DrawString("← → 换道 ↑ 跳跃 ↓ 滑铲 P 暂停 M 静音 F1 调试 F11 全屏",
|
||||
_fTiny, sb, 18, h - 26);
|
||||
|
||||
if (showFps)
|
||||
{
|
||||
using var db = new SolidBrush(Color.FromArgb(185, 168, 255, 196));
|
||||
g.DrawString(
|
||||
$"FPS {world.Fps:0} 障碍 {world.Obstacles.Count} 金币 {world.Coins.Count}" +
|
||||
$" 装饰 {world.Props.Count} 粒子 {world.Particles.Count} 速度 {world.Speed:0}",
|
||||
_fTiny, db, 18, h - 46);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawReady(Graphics g, int w, int h, GameWorld world)
|
||||
{
|
||||
float cy = h * 0.055f; // 贴顶排布,避免压住画面中央的跑者
|
||||
|
||||
DrawCentered(g, "神庙逃亡", _fHuge, w, cy + 3, Color.FromArgb(130, 0, 0, 0));
|
||||
DrawCentered(g, "神庙逃亡", _fHuge, w, cy, Color.FromArgb(255, 232, 176));
|
||||
|
||||
DrawCentered(g, "TEMPLE RUN", _fTitle, w, cy + 58, Color.FromArgb(214, 176, 120));
|
||||
|
||||
string[] lines =
|
||||
{
|
||||
"← → 左右换道",
|
||||
"↑ 或 空格 跳跃越过矮石堆",
|
||||
"↓ 滑铲穿过悬空横梁",
|
||||
"石墙无法越过,只能换道躲避",
|
||||
"收集金币可提高速度倍率"
|
||||
};
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
DrawCentered(g, lines[i], _fMid, w, cy + 118 + i * 30, Color.FromArgb(226, 216, 202));
|
||||
|
||||
// 闪烁提示
|
||||
float blink = (MathF.Sin(world.Clock * 4.2f) + 1f) * 0.5f;
|
||||
var c = Cfg.Lerp(Color.FromArgb(120, 255, 236, 190), Color.FromArgb(255, 255, 244, 214), blink);
|
||||
DrawCentered(g, "按 空格 开始奔跑", _fBig, w, cy + 118 + lines.Length * 30 + 26, c);
|
||||
}
|
||||
|
||||
private static void DrawPaused(Graphics g, int w, int h)
|
||||
{
|
||||
using (var ov = new SolidBrush(Color.FromArgb(140, 10, 10, 18)))
|
||||
g.FillRectangle(ov, 0, 0, w, h);
|
||||
DrawCentered(g, "已 暂 停", _fHuge, w, h * 0.38f, Color.White);
|
||||
DrawCentered(g, "按 P / 空格 继续 按 R 重新开始", _fMid, w, h * 0.38f + 70,
|
||||
Color.FromArgb(210, 226, 218, 206));
|
||||
}
|
||||
|
||||
private static void DrawGameOver(Graphics g, int w, int h, GameWorld world)
|
||||
{
|
||||
bool newRecord = world.Score >= world.Best && world.Score > 0;
|
||||
|
||||
float pw = Math.Min(520f, w - 80f);
|
||||
float ph = 330f;
|
||||
float px = (w - pw) * 0.5f;
|
||||
float py = h * 0.5f - ph * 0.5f;
|
||||
|
||||
using (var ov = new SolidBrush(Color.FromArgb(150, 8, 8, 14)))
|
||||
g.FillRectangle(ov, 0, 0, w, h);
|
||||
|
||||
var panel = new RectangleF(px, py, pw, ph);
|
||||
using (var pp = RoundRect(panel, 18f))
|
||||
{
|
||||
using var pb = new SolidBrush(Color.FromArgb(226, 26, 24, 34));
|
||||
g.FillPath(pb, pp);
|
||||
using var pen = new Pen(Color.FromArgb(200, 214, 168, 96), 2.4f);
|
||||
g.DrawPath(pen, pp);
|
||||
}
|
||||
|
||||
DrawCentered(g, "游 戏 结 束", _fTitle, w, py + 24, Color.FromArgb(255, 138, 106));
|
||||
|
||||
float y = py + 92;
|
||||
DrawStatRow(g, px, pw, y, "本局得分", world.Score.ToString(),
|
||||
newRecord ? Cfg.Gold : Color.White, _fBig);
|
||||
DrawStatRow(g, px, pw, y + 46, "金币数量", world.CoinCount.ToString(), Color.FromArgb(255, 214, 120), _fBig);
|
||||
DrawStatRow(g, px, pw, y + 92, "奔跑距离", ((int)(world.Distance / 10f)) + " m", Color.FromArgb(200, 226, 216, 200), _fBig);
|
||||
DrawStatRow(g, px, pw, y + 138, "历史最佳", world.Best.ToString(), Color.FromArgb(150, 226, 214, 196), _fBig);
|
||||
|
||||
if (newRecord)
|
||||
DrawCentered(g, "★ 新 纪 录 ★", _fMid, w, py + ph - 88, Cfg.Gold);
|
||||
|
||||
DrawCentered(g, "按 R / 空格 再来一局", _fMid, w, py + ph - 46,
|
||||
Color.FromArgb(226, 236, 226, 210));
|
||||
}
|
||||
|
||||
private static void DrawStatRow(Graphics g, float px, float pw, float y, string label, string value,
|
||||
Color valueColor, Font vf)
|
||||
{
|
||||
using (var lb = new SolidBrush(Color.FromArgb(190, 208, 198, 184)))
|
||||
g.DrawString(label, _fMid, lb, px + 36, y + 6);
|
||||
|
||||
SizeF vs = g.MeasureString(value, vf);
|
||||
using (var vb = new SolidBrush(valueColor))
|
||||
g.DrawString(value, vf, vb, px + pw - 36 - vs.Width, y);
|
||||
}
|
||||
|
||||
private static void DrawCentered(Graphics g, string text, Font f, int w, float y, Color color)
|
||||
{
|
||||
using var br = new SolidBrush(color);
|
||||
SizeF sz = g.MeasureString(text, f);
|
||||
g.DrawString(text, f, br, (w - sz.Width) * 0.5f, y);
|
||||
}
|
||||
|
||||
private static void DrawChip(Graphics g, float x, float y, float w, float h)
|
||||
{
|
||||
var r = new RectangleF(x, y, w, h);
|
||||
using var p = RoundRect(r, 10f);
|
||||
using var br = new SolidBrush(Color.FromArgb(118, 12, 14, 22));
|
||||
g.FillPath(br, p);
|
||||
using var pen = new Pen(Color.FromArgb(86, 226, 198, 140), 1.4f);
|
||||
g.DrawPath(pen, p);
|
||||
}
|
||||
|
||||
private static GraphicsPath RoundRect(RectangleF r, float radius)
|
||||
{
|
||||
var p = new GraphicsPath();
|
||||
float d = Math.Min(radius * 2f, Math.Min(r.Width, r.Height));
|
||||
p.AddArc(r.X, r.Y, d, d, 180, 90);
|
||||
p.AddArc(r.Right - d, r.Y, d, d, 270, 90);
|
||||
p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
|
||||
p.AddArc(r.X, r.Bottom - d, d, d, 90, 90);
|
||||
p.CloseFigure();
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace TempleRun;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.SetHighDpiMode(HighDpiMode.SystemAware);
|
||||
|
||||
Application.Run(new GameForm());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblyName>TempleRun</AssemblyName>
|
||||
<RootNamespace>TempleRun</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<SatelliteResourceLanguages>zh-Hans</SatelliteResourceLanguages>
|
||||
<!-- 关闭自动生成的 ApplicationConfiguration,改由 Program.cs 手工初始化 -->
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1,53 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class Win32 {
|
||||
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
||||
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r);
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
}
|
||||
"@
|
||||
|
||||
$root = "c:\Users\32453\CodeBuddy\20260825142915\TempleRun"
|
||||
$exe = Join-Path $root "bin\Release\net8.0-windows\TempleRun.exe"
|
||||
$out = Join-Path $root "_shot"
|
||||
New-Item -ItemType Directory -Force -Path $out | Out-Null
|
||||
|
||||
$p = Start-Process -FilePath $exe -PassThru
|
||||
Start-Sleep -Seconds 3
|
||||
$p.Refresh()
|
||||
|
||||
function Grab([string]$name) {
|
||||
$h = $p.MainWindowHandle
|
||||
if ($h -eq [IntPtr]::Zero) { Write-Output "no window"; return }
|
||||
[Win32]::SetForegroundWindow($h) | Out-Null
|
||||
Start-Sleep -Milliseconds 700
|
||||
$r = New-Object 'Win32+RECT'
|
||||
[Win32]::GetWindowRect($h, [ref]$r) | Out-Null
|
||||
$w = $r.Right - $r.Left
|
||||
$ht = $r.Bottom - $r.Top
|
||||
if ($w -le 0 -or $ht -le 0) { Write-Output "bad rect $w x $ht"; return }
|
||||
$bmp = New-Object System.Drawing.Bitmap $w, $ht
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen($r.Left, $r.Top, 0, 0, $bmp.Size)
|
||||
$path = Join-Path $out "$name.png"
|
||||
$bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
Write-Output "saved $name : $w x $ht"
|
||||
}
|
||||
|
||||
Grab "01_ready"
|
||||
|
||||
[System.Windows.Forms.SendKeys]::SendWait(" ")
|
||||
Start-Sleep -Seconds 4
|
||||
Grab "02_playing"
|
||||
|
||||
Start-Sleep -Seconds 4
|
||||
Grab "03_playing2"
|
||||
|
||||
try { $p.Kill() } catch {}
|
||||
Write-Output "exit=$($p.HasExited)"
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="TempleRun.app" />
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 / 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user