地图设计
Stardew Valley 风格地图:看起来随意自然,内在有逻辑。
设计哲学
"有组织的混乱" — 地图不追求对称网格,追求有机自然的感觉。
核心原则:
- 分区不清边界:用树木、河流、地形变化自然过渡,不用围墙
- 路径引导:弯曲小路连接各区域,玩家自然知道该往哪走
- 密度不均:镇中心建筑密集,野外稀疏,留白也是设计
- 层叠遮挡:树冠遮挡角色,屋顶遮挡行人,增加纵深感
参考游戏:Stardew Valley、Eastward、Animal Crossing
地图结构
整体布局
┌─────────────────────────────────────────────┐
│ 山 区 │
│ (矿区、瞭望塔、野营地) │
├──────────┬────────────────────┬──────────────┤
│ │ │ │
│ 住宅区 │ 商业中心 │ 自然区 │
│ │ │ │
│ 家、旅店 │ 广场、酒馆、药草铺 │ 林间空地 │
│ 公寓 │ 裁缝店、市集 │ 农场 │
│ │ 图书馆、神殿 │ 码头 │
│ │ │ │
├──────────┴────────┬───────────┴──────────────┤
│ │ │
│ 工业区 │ 墓地 │
│ 锻造坊、仓库 │ (氛围阴暗) │
│ │ │
└───────────────────┴──────────────────────────┘区域定义
| 区域 | 氛围 | 建筑密度 | 自然元素 |
|---|---|---|---|
| 住宅区 | 温馨、安静 | 中等 | 花园、小树 |
| 商业中心 | 热闹、繁忙 | 高 | 盆栽、路灯 |
| 工业区 | 粗犷、实用 | 低 | 少,烟囱 |
| 自然区 | 开阔、田园 | 极低 | 大量树木、水域 |
| 墓地 | 阴暗、肃穆 | 低 | 枯树、苔藓 |
| 山区 | 险峻、神秘 | 极低 | 岩石、松树 |
层叠渲染系统
地图不是平面的,有前后遮挡关系。
渲染层级
Layer 0: 地形基底
└─ 草地 tile(季节化颜色)
Layer 1: 道路/路径
└─ 泥路、石板路、桥
Layer 2: 水域
└─ 河流、池塘、码头水域(季节化)
Layer 3: 建筑阴影
└─ 每栋建筑下方的投影
Layer 4: 地面装饰
└─ 围栏、花坛、路灯、石头
Layer 5: 低层自然物
└─ 灌木、草地装饰、蘑菇
Layer 6: 建筑主体
└─ 所有建筑(有碰撞体积)
Layer 7: 角色/NPC
└─ 玩家和 NPC
Layer 8: 高层自然物
└─ 树冠、高层建筑屋顶
Layer 9: 天气/氛围
└─ 粒子、遮罩遮挡逻辑
ts
function getSortY(entity: Entity): number {
// 用实体底部的 y 坐标排序
// 底部越靠下的实体,越晚绘制(在上层)
return entity.y + entity.height;
}
// 建筑和角色混排时,按 sortY 排序
// 树冠始终在最上层,不管 sortY建筑布局规则
不规则网格
建筑不严格对齐网格,允许小范围偏移:
ts
interface BuildingPlacement {
locationId: string;
// 地图坐标(像素)
x: number;
y: number;
// 允许的偏移范围(像素)
offsetX: number; // -8 ~ +8
offsetY: number; // -8 ~ +8
// 尺寸(tiles)
width: number; // 2-3 tiles
height: number; // 3-4 tiles
// 朝向
facing: 'north' | 'south' | 'east' | 'west';
}
// 建筑坐标 = 基准网格坐标 + 随机偏移(基于 locationId 的 hash)
function getBuildingPosition(placement: BuildingPlacement): { x: number; y: number } {
const hash = hashString(placement.locationId);
const ox = ((hash % 17) - 8); // -8 ~ +8 像素偏移
const oy = ((hash % 13) - 6); // -6 ~ +6 像素偏移
return {
x: placement.x + ox,
y: placement.y + oy,
};
}建筑间距
同类建筑:间距 1-2 tiles(密集感)
不同类建筑:间距 2-4 tiles
建筑与道路:建筑门口紧贴道路
建筑与水域:至少 2 tiles 缓冲道路系统
道路不是直线网格,是弯曲的连接线。
道路类型
| 类型 | 宽度 | 用途 |
|---|---|---|
| 主路 | 2 tiles | 连接主要区域 |
| 小径 | 1 tile | 区域内部连接 |
| 桥 | 2 tiles | 跨水域 |
道路生成
ts
interface PathSegment {
from: { x: number; y: number };
to: { x: number; y: number };
controlPoint?: { x: number; y: number }; // 贝塞尔曲线控制点
width: number; // tiles
type: 'main' | 'trail' | 'bridge';
}
// 用贝塞尔曲线生成弯曲路径
function generatePathPoints(segment: PathSegment): Point[] {
const points: Point[] = [];
const steps = 20;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const p = bezierPoint(segment.from, segment.controlPoint, segment.to, t);
points.push(p);
}
return points;
}自然装饰分布
自然元素不是随机撒的,有密度规则。
ts
const DECORATION_DENSITY: Record<ZoneType, number> = {
residential: 0.05, // 5% 覆盖率,少量花园植物
commercial: 0.02, // 2%,主要是盆栽和路灯
industrial: 0.01, // 1%,几乎无装饰
nature: 0.40, // 40%,大量树木灌木
cemetery: 0.15, // 15%,枯树和苔藓
mountain: 0.25, // 25%,岩石和松树
};装饰分布算法
ts
function placeDecorations(zone: Zone, seed: number): Decoration[] {
const decorations: Decoration[] = [];
const density = DECORATION_DENSITY[zone.type];
const rng = seededRandom(seed);
for (let x = zone.bounds.x; x < zone.bounds.right; x += TILE) {
for (let y = zone.bounds.y; y < zone.bounds.bottom; y += TILE) {
if (rng() < density) {
// 避免在道路上放置
if (!isOnRoad(x, y)) {
decorations.push({
type: pickDecorationType(zone.type, rng),
x: x + rng() * 8, // 小范围随机偏移
y: y + rng() * 8,
});
}
}
}
}
return decorations;
}摄像机与视口
视口系统
ts
interface Camera {
x: number; // 视口左上角世界坐标
y: number;
zoom: number; // 缩放倍率 0.5-2.0
targetX: number; // 平滑跟随目标
targetY: number;
}
// 只渲染视口范围内的实体(性能优化)
function getVisibleEntities(camera: Camera, entities: Entity[]): Entity[] {
const margin = 2 * TILE; // 视口外2格的缓冲区
return entities.filter(e =>
e.x + e.width > camera.x - margin &&
e.x < camera.x + SCREEN_W / camera.zoom + margin &&
e.y + e.height > camera.y - margin &&
e.y < camera.y + SCREEN_H / camera.zoom + margin
);
}地图尺寸
世界尺寸:80×60 tiles = 2560×1920 px
视口尺寸:约 25×18 tiles = 800×576 px
缩放范围:0.5x - 2.0x植被密度表(按区域 × 季节)
| 区域 | 春 | 夏 | 秋 | 冬 |
|---|---|---|---|---|
| 住宅区 | 花园开花 | 草坪茂盛 | 落叶铺地 | 枯草薄雪 |
| 商业中心 | 盆栽开花 | 绿意盎然 | 落叶飘散 | 光秃+雪 |
| 自然区 | 花海+新叶 | 密林 | 红叶满地 | 枯枝积雪 |
| 墓地 | 苔藓复苏 | 杂草丛生 | 萧瑟 | 荒凉积雪 |
| 山区 | 野花 | 青松 | 金黄松针 | 白雪皑皑 |
数据结构
ts
// src/config/mapConfig.ts
export interface MapConfig {
worldWidth: number; // tiles
worldHeight: number; // tiles
tileSize: number; // px
zones: Zone[];
buildings: BuildingPlacement[];
paths: PathSegment[];
decorations: Decoration[];
}
export interface Zone {
id: string;
type: ZoneType;
bounds: { x: number; y: number; width: number; height: number };
ambientSound?: string;
}