Skip to content

地图设计

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

📐 季节对地图的影响见 季节系统 📐 天气效果见 天气系统 🎨 建筑素材见 建筑精灵规范

Released under the MIT License.