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

903 lines
35 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}