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;
}
}