using System.Drawing.Drawing2D; using System.Drawing.Text; namespace TempleRun; /// /// 全部绘制逻辑。使用 GDI+ 手工做透视投影,不依赖任何 3D 库。 /// 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(); 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; } }