前言
《城市:天际线》(Cities: Skylines) 是最成功的城市建造模拟游戏之一。
你有没有想过:
- 几十万市民是怎么"活"起来的?
- 交通堵塞是怎么模拟的?
- 水电网络是怎么计算的?
- 为什么我的城市会死亡螺旋?
今天从系统架构 到核心算法,揭秘这类游戏的运行原理。
一、整体架构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 城市模拟游戏核心架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 游戏主循环 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 输入处理 │→ │ 系统更新 │→ │ 渲染绘制 │→ │ UI更新 │→ ... │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 核心模拟系统 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 人口系统 │ │ 交通系统 │ │ 经济系统 │ │ │
│ │ │ Population │ │ Traffic │ │ Economy │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • 市民Agent │ │ • 道路网络 │ │ • 税收 │ │ │
│ │ │ • 需求计算 │ │ • 寻路算法 │ │ • 就业 │ │ │
│ │ │ • 人口增长 │ │ • 拥堵模拟 │ │ • 土地价值 │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────────┼────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 公共服务 │ │ 资源网络 │ │ 区域系统 │ │ │
│ │ │ Services │ │ Resources │ │ Zoning │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • 消防/医疗 │ │ • 电力网格 │ │ • 住宅区 │ │ │
│ │ │ • 教育/警察 │ │ • 供水排污 │ │ • 商业区 │ │ │
│ │ │ • 覆盖范围 │ │ • 资源传播 │ │ • 工业区 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 数据层 │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 地图网格 │ │ 建筑数据 │ │ Agent池 │ │ │
│ │ │ Grid Map │ │ Buildings │ │ Agent Pool │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
二、地图网格系统
┌─────────────────────────────────────────────────────────────────────────────┐
│ 地图网格系统 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【分层网格结构】 │
│ ───────────────────────────────────────── │
│ │
│ 城市模拟通常使用多层网格存储不同数据: │
│ │
│ Layer 0: 地形层 (Terrain) │
│ ┌───┬───┬───┬───┬───┐ │
│ │ 5 │ 5 │ 4 │ 3 │ 2 │ 高度值 │
│ ├───┼───┼───┼───┼───┤ │
│ │ 5 │ 4 │ 3 │ 2 │ 1 │ │
│ ├───┼───┼───┼───┼───┤ │
│ │ 4 │ 3 │ 2 │ 1 │ 0 │ 0=水面 │
│ └───┴───┴───┴───┴───┘ │
│ │
│ Layer 1: 区划层 (Zoning) │
│ ┌───┬───┬───┬───┬───┐ │
│ │ R │ R │ C │ - │ - │ R=住宅 C=商业 I=工业 │
│ ├───┼───┼───┼───┼───┤ O=办公 -=未规划 │
│ │ R │ R │ C │ C │ - │ │
│ ├───┼───┼───┼───┼───┤ │
│ │ I │ I │ - │ - │ - │ │
│ └───┴───┴───┴───┴───┘ │
│ │
│ Layer 2: 道路层 (Roads) │
│ ┌───┬───┬───┬───┬───┐ │
│ │ │ ║ │ │ │ │ ═ 横向道路 │
│ ├───┼───┼───┼───┼───┤ ║ 纵向道路 │
│ │═══╬═══╬═══╬═══│ │ ╬ 交叉口 │
│ ├───┼───┼───┼───┼───┤ │
│ │ │ ║ │ │ │ │ │
│ └───┴───┴───┴───┴───┘ │
│ │
│ Layer 3: 建筑层 (Buildings) │
│ Layer 4: 电力层 (Power) │
│ Layer 5: 水网层 (Water) │
│ Layer 6: 污染层 (Pollution) │
│ Layer 7: 土地价值层 (Land Value) │
│ ... │
│ │
│ 【网格单元数据结构】 │
│ ───────────────────────────────────────── │
│ │
│ 每个网格单元存储: │
│ - 地形高度 │
│ - 区划类型 │
│ - 建筑ID (如果有) │
│ - 道路连接信息 │
│ - 资源覆盖 (电/水/服务) │
│ - 环境参数 (噪音/污染/土地价值) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
三、人口与Agent系统
┌─────────────────────────────────────────────────────────────────────────────┐
│ Agent-Based Simulation │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【市民Agent模型】 │
│ ───────────────────────────────────────── │
│ │
│ 每个市民是一个独立的Agent,拥有: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Citizen Agent │ │
│ │ ───────────────────────────────────────── │ │
│ │ 属性: │ │
│ │ • id: 唯一标识 │ │
│ │ • age: 年龄 (影响行为模式) │ │
│ │ • education: 教育水平 (影响工作类型) │ │
│ │ • wealth: 财富 (影响住房选择) │ │
│ │ • health: 健康值 │ │
│ │ • happiness: 幸福度 │ │
│ │ │ │
│ │ 位置: │ │
│ │ • home: 住所建筑ID │ │
│ │ • workplace: 工作地点ID │ │
│ │ • current_position: 当前位置 │ │
│ │ │ │
│ │ 状态机: │ │
│ │ • state: HOME / WORKING / COMMUTING / SHOPPING / LEISURE │ │
│ │ • schedule: 日程表 │ │
│ │ │ │
│ │ 行为: │ │
│ │ • find_home() 找住所 │ │
│ │ • find_job() 找工作 │ │
│ │ • commute() 通勤 │ │
│ │ • consume() 消费 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【Agent日程模拟】 │
│ ───────────────────────────────────────── │
│ │
│ 时间 0 4 8 12 16 20 24 │
│ │ │ │ │ │ │ │ │
│ 工人 ████████ ████████████ │
│ 睡觉 │通勤│ 工作 │购物│ 睡觉 │
│ │
│ 学生 ████████ ████████████ │
│ 睡觉 │通勤│ 上学 │娱乐│ 睡觉 │
│ │
│ 退休 ████████████████████████████ │
│ 睡觉 │ 休闲活动 │ 购物 │睡觉 │
│ │
│ 【Agent数量优化】 │
│ ───────────────────────────────────────── │
│ │
│ 问题: 10万市民 = 10万Agent,性能爆炸! │
│ │
│ 解决方案: │
│ 1. 代表性Agent: 1个Agent代表多个市民 (1:100) │
│ 2. 统计模拟: 大部分用统计模型,只渲染可见Agent │
│ 3. LOD系统: 远处用聚合数据,近处用真实Agent │
│ 4. 分帧更新: 每帧只更新部分Agent │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
四、交通系统(核心难点)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 交通系统 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【道路网络图】 │
│ ───────────────────────────────────────── │
│ │
│ 道路系统建模为有向图: │
│ │
│ 节点(Node): 交叉口、道路端点 │
│ 边(Edge): 道路段 │
│ │
│ [N1]═══════[N2]═══════[N3] │
│ ║ ║ ║ │
│ ║ E1 ║ E2 ║ │
│ ║ ║ ║ │
│ [N4]═══════[N5]═══════[N6] │
│ ║ │
│ ║ E3 │
│ ║ │
│ [N7] │
│ │
│ 边的属性: │
│ - 长度、车道数、限速 │
│ - 当前车辆数、拥堵系数 │
│ - 道路类型 (高速/主干/支路) │
│ │
│ 【寻路算法: A*】 │
│ ───────────────────────────────────────── │
│ │
│ f(n) = g(n) + h(n) │
│ │
│ g(n): 从起点到n的实际代价 │
│ h(n): 从n到终点的估计代价 (启发函数) │
│ │
│ 代价计算 (关键!): │
│ ───────────────────────────────────────── │
│ │
│ cost = base_time × congestion_factor × road_type_weight │
│ │
│ base_time = distance / speed_limit │
│ congestion_factor = 1 + (current_vehicles / capacity)² │
│ road_type_weight: 高速=0.8, 主干=1.0, 支路=1.2 │
│ │
│ 【拥堵模拟】 │
│ ───────────────────────────────────────── │
│ │
│ 实际速度 = 自由流速度 × (1 - (流量/容量)^α) │
│ │
│ 流量 < 容量: 车辆正常通行 │
│ 流量 ≈ 容量: 开始拥堵,速度下降 │
│ 流量 > 容量: 严重拥堵,形成排队 │
│ │
│ 流量(辆/小时) │
│ ▲ │
│ │ ╭──────╮ 容量上限 │
│ │ ╱ ╲ │
│ │ ╱ ╲ │
│ │ ╱ ╲ 拥堵区域 │
│ │ ╱ │
│ │ ╱ 自由流区域 │
│ │╱ │
│ └─────────────────────────────────────► 密度(辆/km) │
│ │
│ 【交叉口模拟】 │
│ ───────────────────────────────────────── │
│ │
│ 信号灯控制: │
│ - 固定配时: 预设红绿灯周期 │
│ - 感应控制: 根据实时流量调整 │
│ - 自适应控制: AI优化信号配时 │
│ │
│ 让行规则: │
│ - 直行优先 │
│ - 主路优先 │
│ - 右转让直行 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
五、资源网络系统
┌─────────────────────────────────────────────────────────────────────────────┐
│ 资源网络系统 (电力/供水) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【电力网络】 │
│ ───────────────────────────────────────── │
│ │
│ 电力流动模型 (简化): │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [发电厂] ──电塔── [变电站] ──电线── [建筑] │ │
│ │ 500MW -100MW -50MW │ │
│ │ │ │
│ │ 电力传播算法 (BFS/洪水填充): │ │
│ │ │ │
│ │ 1. 从发电厂开始,标记为"有电" │ │
│ │ 2. 沿电网扩散到相邻建筑 │ │
│ │ 3. 计算每个节点的供/需平衡 │ │
│ │ 4. 需求>供给时,末端建筑断电 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ 【供水网络】 │
│ ───────────────────────────────────────── │
│ │
│ 水流模型 (需考虑压力): │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [水泵站] ──水管── [水塔] ──水管── [建筑] │ │
│ │ 生产水 存储/加压 消耗水 │ │
│ │ │ │
│ │ [建筑] ──污水管── [污水厂] │ │
│ │ 产生污水 处理污水 │ │
│ │ │ │
│ │ 水压计算: │ │
│ │ - 水泵提供初始压力 │ │
│ │ - 压力随距离衰减 │ │
│ │ - 高度差影响压力 (重力) │ │
│ │ - 压力不足时供水不足 │ │
│ │ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ 【服务覆盖系统】 │
│ ───────────────────────────────────────── │
│ │
│ 消防站、医院、学校等服务建筑有覆盖范围: │
│ │
│ 服务覆盖度 │
│ ▲ │
│ 100%├──╮ │
│ │ ╲ │
│ │ ╲ │
│ │ ╲ │
│ │ ╲ │
│ 0% └───────┴──────────► 距离 │
│ 0 R_max │
│ │
│ coverage(d) = max(0, 1 - d/R_max) │
│ 或: coverage(d) = exp(-d²/2σ²) (高斯衰减) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
六、经济与需求系统
┌─────────────────────────────────────────────────────────────────────────────┐
│ 经济与需求系统 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【RCI需求模型】 │
│ ───────────────────────────────────────── │
│ │
│ R = Residential (住宅需求) │
│ C = Commercial (商业需求) │
│ I = Industrial (工业需求) │
│ │
│ 需求条 (游戏UI常见): │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ R: ████████████░░░░░░ (高需求) │ │
│ │ C: ██████░░░░░░░░░░░░ (中需求) │ │
│ │ I: ████░░░░░░░░░░░░░░ (低需求) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【需求计算公式】 │
│ ───────────────────────────────────────── │
│ │
│ 住宅需求 R_demand: │
│ R_demand = base_growth │
│ + job_availability × w1 (有工作机会吸引人口) │
│ + service_quality × w2 (服务质量好吸引人) │
│ - unemployment × w3 (失业率高赶走人) │
│ - pollution × w4 (污染赶走人) │
│ - crime × w5 (犯罪赶走人) │
│ │
│ 商业需求 C_demand: │
│ C_demand = population × consumption_rate │
│ - commercial_capacity (现有商业容量) │
│ + tourism (旅游业) │
│ │
│ 工业需求 I_demand: │
│ I_demand = commercial_demand × production_ratio │
│ + export_demand (出口需求) │
│ - industrial_capacity (现有工业容量) │
│ │
│ 【经济循环】 │
│ ───────────────────────────────────────── │
│ │
│ ┌──────────────┐ │
│ │ 工业 I │ │
│ │ 生产商品 │ │
│ └──────┬───────┘ │
│ │ 商品 │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 住宅 R │ ←── │ 商业 C │ │
│ │ 居民消费 │ 商品 │ 零售服务 │ │
│ │ 提供劳动力 │ ───→ │ 提供就业 │ │
│ └──────────────┘ 劳动 └──────────────┘ │
│ │ ▲ │
│ │ 税收 │ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 政府财政 │ │
│ │ 建设/维护 │ │
│ └──────────────┘ │
│ │
│ 【土地价值计算】 │
│ ───────────────────────────────────────── │
│ │
│ land_value = base_value │
│ + accessibility × w1 (交通便利性) │
│ + services × w2 (公共服务覆盖) │
│ + parks × w3 (公园/景观) │
│ - pollution × w4 (污染) │
│ - noise × w5 (噪音) │
│ - crime × w6 (犯罪率) │
│ │
│ 高地价 → 吸引高密度/高档建筑 │
│ 低地价 → 只能建低档建筑或工业 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
七、建筑生成系统
┌─────────────────────────────────────────────────────────────────────────────┐
│ 建筑生成系统 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【建筑升级机制】 │
│ ───────────────────────────────────────── │
│ │
│ 建筑等级由多个因素决定: │
│ │
│ Level 1 → Level 2 → Level 3 → Level 4 → Level 5 │
│ 小木屋 公寓 高层公寓 豪华公寓 摩天大楼 │
│ │
│ 升级条件: │
│ - 土地价值达到阈值 │
│ - 服务覆盖完善 (电/水/教育/医疗) │
│ - 交通便利 │
│ - 环境质量好 │
│ - 存在时间足够长 │
│ │
│ 【建筑选择算法】 │
│ ───────────────────────────────────────── │
│ │
│ 当区域满足建造条件时: │
│ │
│ 1. 确定区划类型 (R/C/I) │
│ 2. 计算地块等级 (根据土地价值等) │
│ 3. 从对应等级的建筑池中随机选择 │
│ 4. 检查地块尺寸是否匹配 │
│ 5. 放置建筑,开始建造动画 │
│ │
│ 建筑池示例: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 住宅 Level 1: [小木屋A, 小木屋B, 平房A, 平房B, ...] │ │
│ │ 住宅 Level 2: [公寓A, 公寓B, 联排别墅A, ...] │ │
│ │ 住宅 Level 3: [高层A, 高层B, 高层C, ...] │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【废弃与拆除】 │
│ ───────────────────────────────────────── │
│ │
│ 建筑废弃条件: │
│ - 长期无电/无水 │
│ - 严重污染 │
│ - 高犯罪率 │
│ - 无人居住/无人工作 (需求不足) │
│ - 火灾/灾害损坏 │
│ │
│ 废弃建筑 → 降低周围土地价值 → 可能引发连锁废弃 (死亡螺旋!) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
八、代码实现
核心数据结构
python
"""
城市模拟游戏核心系统
"""
import math
import heapq
import random
from enum import Enum, auto
from dataclasses import dataclass, field
from typing import List, Dict, Set, Tuple, Optional
from collections import defaultdict
# ============================================================
# 枚举定义
# ============================================================
class ZoneType(Enum):
NONE = 0
RESIDENTIAL = 1 # 住宅
COMMERCIAL = 2 # 商业
INDUSTRIAL = 3 # 工业
OFFICE = 4 # 办公
class BuildingState(Enum):
CONSTRUCTING = auto()
ACTIVE = auto()
ABANDONED = auto()
DEMOLISHED = auto()
class CitizenState(Enum):
HOME = auto()
WORKING = auto()
COMMUTING = auto()
SHOPPING = auto()
LEISURE = auto()
class RoadType(Enum):
SMALL = 1 # 小路 2车道
MEDIUM = 2 # 中路 4车道
LARGE = 3 # 大路 6车道
HIGHWAY = 4 # 高速 8车道
# ============================================================
# 基础数据结构
# ============================================================
@dataclass
class Vector2:
x: float
y: float
def distance_to(self, other: 'Vector2') -> float:
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
def __hash__(self):
return hash((self.x, self.y))
@dataclass
class GridCell:
"""网格单元"""
x: int
y: int
terrain_height: float = 0.0
zone_type: ZoneType = ZoneType.NONE
building_id: Optional[int] = None
road_id: Optional[int] = None
# 资源覆盖
has_power: bool = False
has_water: bool = False
water_pressure: float = 0.0
# 环境
pollution: float = 0.0
noise: float = 0.0
land_value: float = 50.0
crime_rate: float = 0.0
# 服务覆盖
fire_coverage: float = 0.0
health_coverage: float = 0.0
education_coverage: float = 0.0
police_coverage: float = 0.0
# ============================================================
# 建筑系统
# ============================================================
@dataclass
class Building:
"""建筑"""
id: int
position: Vector2
size: Tuple[int, int] # 占用网格数
zone_type: ZoneType
level: int = 1
state: BuildingState = BuildingState.CONSTRUCTING
# 容量
max_residents: int = 0 # 住宅容量
max_workers: int = 0 # 工作岗位
max_customers: int = 0 # 顾客容量
# 当前
residents: List[int] = field(default_factory=list)
workers: List[int] = field(default_factory=list)
# 资源消耗
power_consumption: int = 0
water_consumption: int = 0
# 生产 (工业/商业)
production_rate: float = 0.0
# 统计
happiness: float = 50.0
construction_progress: float = 0.0
class BuildingManager:
"""建筑管理器"""
# 建筑模板
TEMPLATES = {
(ZoneType.RESIDENTIAL, 1): {'residents': 4, 'power': 5, 'water': 3},
(ZoneType.RESIDENTIAL, 2): {'residents': 12, 'power': 15, 'water': 10},
(ZoneType.RESIDENTIAL, 3): {'residents': 40, 'power': 50, 'water': 35},
(ZoneType.RESIDENTIAL, 4): {'residents': 100, 'power': 120, 'water': 80},
(ZoneType.RESIDENTIAL, 5): {'residents': 250, 'power': 300, 'water': 200},
(ZoneType.COMMERCIAL, 1): {'workers': 5, 'customers': 20, 'power': 10, 'water': 5},
(ZoneType.COMMERCIAL, 2): {'workers': 15, 'customers': 60, 'power': 30, 'water': 15},
(ZoneType.COMMERCIAL, 3): {'workers': 50, 'customers': 200, 'power': 100, 'water': 50},
(ZoneType.INDUSTRIAL, 1): {'workers': 20, 'power': 50, 'water': 30, 'production': 100},
(ZoneType.INDUSTRIAL, 2): {'workers': 50, 'power': 150, 'water': 80, 'production': 300},
(ZoneType.INDUSTRIAL, 3): {'workers': 100, 'power': 400, 'water': 200, 'production': 800},
}
def __init__(self):
self.buildings: Dict[int, Building] = {}
self.next_id = 1
def create_building(self, position: Vector2, zone_type: ZoneType,
level: int = 1) -> Building:
"""创建建筑"""
template = self.TEMPLATES.get((zone_type, level), {})
building = Building(
id=self.next_id,
position=position,
size=(1, 1),
zone_type=zone_type,
level=level,
max_residents=template.get('residents', 0),
max_workers=template.get('workers', 0),
max_customers=template.get('customers', 0),
power_consumption=template.get('power', 0),
water_consumption=template.get('water', 0),
production_rate=template.get('production', 0)
)
self.buildings[self.next_id] = building
self.next_id += 1
return building
def calculate_upgrade_level(self, cell: GridCell) -> int:
"""根据条件计算建筑等级"""
level = 1
# 土地价值影响等级
if cell.land_value > 70:
level = 2
if cell.land_value > 85:
level = 3
if cell.land_value > 95:
level = 4
if cell.land_value > 99:
level = 5
# 必须有基础设施
if not cell.has_power or not cell.has_water:
level = min(level, 1)
# 服务覆盖影响
service_avg = (cell.fire_coverage + cell.health_coverage +
cell.education_coverage + cell.police_coverage) / 4
if service_avg < 0.3:
level = min(level, 2)
return level
def update_building(self, building: Building, dt: float):
"""更新建筑状态"""
if building.state == BuildingState.CONSTRUCTING:
building.construction_progress += dt * 0.1
if building.construction_progress >= 1.0:
building.state = BuildingState.ACTIVE
elif building.state == BuildingState.ACTIVE:
# 计算幸福度
self._update_happiness(building)
# 检查废弃条件
if building.happiness < 10:
building.state = BuildingState.ABANDONED
def _update_happiness(self, building: Building):
"""更新建筑幸福度"""
# 简化的幸福度计算
happiness = 50.0
# 居住率影响
if building.max_residents > 0:
occupancy = len(building.residents) / building.max_residents
if occupancy < 0.3:
happiness -= 20 # 人太少,说明有问题
# 就业情况影响
if building.max_workers > 0:
employment = len(building.workers) / building.max_workers
happiness += (employment - 0.5) * 20
building.happiness = max(0, min(100, happiness))
# ============================================================
# 市民Agent系统
# ============================================================
@dataclass
class Citizen:
"""市民Agent"""
id: int
age: int
education: int # 0-3: 无/小学/中学/大学
wealth: float
health: float = 100.0
happiness: float = 50.0
# 位置
home_id: Optional[int] = None
workplace_id: Optional[int] = None
current_position: Optional[Vector2] = None
# 状态
state: CitizenState = CitizenState.HOME
state_timer: float = 0.0
# 通勤
commute_path: List[int] = field(default_factory=list)
commute_progress: float = 0.0
class CitizenManager:
"""市民管理器"""
def __init__(self, building_manager: BuildingManager):
self.citizens: Dict[int, Citizen] = {}
self.building_manager = building_manager
self.next_id = 1
# 日程表 (小时 -> 状态)
self.schedules = {
'worker': {
(0, 7): CitizenState.HOME,
(7, 8): CitizenState.COMMUTING,
(8, 17): CitizenState.WORKING,
(17, 18): CitizenState.COMMUTING,
(18, 20): CitizenState.SHOPPING,
(20, 24): CitizenState.HOME,
},
'student': {
(0, 7): CitizenState.HOME,
(7, 8): CitizenState.COMMUTING,
(8, 15): CitizenState.WORKING, # 上学
(15, 16): CitizenState.COMMUTING,
(16, 20): CitizenState.LEISURE,
(20, 24): CitizenState.HOME,
},
'retired': {
(0, 8): CitizenState.HOME,
(8, 12): CitizenState.LEISURE,
(12, 14): CitizenState.SHOPPING,
(14, 18): CitizenState.LEISURE,
(18, 24): CitizenState.HOME,
}
}
def spawn_citizen(self, home_id: int) -> Citizen:
"""生成新市民"""
age = random.randint(18, 65)
education = random.choices([0, 1, 2, 3], weights=[10, 30, 40, 20])[0]
wealth = random.gauss(50, 20)
citizen = Citizen(
id=self.next_id,
age=age,
education=education,
wealth=max(10, wealth),
home_id=home_id
)
self.citizens[self.next_id] = citizen
self.next_id += 1
# 添加到建筑
if home_id in self.building_manager.buildings:
self.building_manager.buildings[home_id].residents.append(citizen.id)
return citizen
def find_job(self, citizen: Citizen) -> bool:
"""为市民找工作"""
if citizen.workplace_id is not None:
return True
# 根据教育水平寻找匹配的工作
suitable_buildings = []
for building in self.building_manager.buildings.values():
if building.state != BuildingState.ACTIVE:
continue
if len(building.workers) >= building.max_workers:
continue
# 工业需要低教育,办公需要高教育
if building.zone_type == ZoneType.INDUSTRIAL and citizen.education <= 1:
suitable_buildings.append(building)
elif building.zone_type == ZoneType.COMMERCIAL:
suitable_buildings.append(building)
elif building.zone_type == ZoneType.OFFICE and citizen.education >= 2:
suitable_buildings.append(building)
if suitable_buildings:
# 选择最近的
home = self.building_manager.buildings.get(citizen.home_id)
if home:
suitable_buildings.sort(
key=lambda b: b.position.distance_to(home.position)
)
workplace = suitable_buildings[0]
citizen.workplace_id = workplace.id
workplace.workers.append(citizen.id)
return True
return False
def update_citizen(self, citizen: Citizen, game_hour: float, dt: float):
"""更新市民状态"""
# 确定日程类型
if citizen.age >= 65:
schedule_type = 'retired'
elif citizen.age < 18:
schedule_type = 'student'
else:
schedule_type = 'worker'
# 根据时间确定应该处于的状态
schedule = self.schedules[schedule_type]
target_state = CitizenState.HOME
for (start, end), state in schedule.items():
if start <= game_hour < end:
target_state = state
break
# 状态转换
if citizen.state != target_state:
self._change_state(citizen, target_state)
# 更新当前状态
citizen.state_timer += dt
# 更新幸福度
self._update_happiness(citizen)
def _change_state(self, citizen: Citizen, new_state: CitizenState):
"""状态转换"""
citizen.state = new_state
citizen.state_timer = 0.0
if new_state == CitizenState.COMMUTING:
# 规划通勤路线 (这里简化)
citizen.commute_progress = 0.0
def _update_happiness(self, citizen: Citizen):
"""更新幸福度"""
happiness = 50.0
# 有工作加分
if citizen.workplace_id is not None:
happiness += 10
elif citizen.age < 65:
happiness -= 20 # 失业扣分
# 健康影响
happiness += (citizen.health - 50) * 0.2
# 财富影响
happiness += (citizen.wealth - 50) * 0.1
citizen.happiness = max(0, min(100, happiness))
# ============================================================
# 交通系统
# ============================================================
@dataclass
class RoadSegment:
"""道路段"""
id: int
start_node: int
end_node: int
road_type: RoadType
length: float
# 容量与流量
capacity: int = 100 # 最大车辆数
current_vehicles: int = 0
@property
def lanes(self) -> int:
return {
RoadType.SMALL: 2,
RoadType.MEDIUM: 4,
RoadType.LARGE: 6,
RoadType.HIGHWAY: 8,
}[self.road_type]
@property
def speed_limit(self) -> float:
return {
RoadType.SMALL: 30,
RoadType.MEDIUM: 50,
RoadType.LARGE: 60,
RoadType.HIGHWAY: 100,
}[self.road_type]
@property
def congestion_factor(self) -> float:
"""拥堵系数 (1.0 = 畅通, >1 = 拥堵)"""
if self.capacity == 0:
return 1.0
ratio = self.current_vehicles / self.capacity
return 1 + ratio ** 2 # 非线性增长
@property
def current_speed(self) -> float:
"""当前实际速度"""
return self.speed_limit / self.congestion_factor
@dataclass
class RoadNode:
"""道路节点 (交叉口)"""
id: int
position: Vector2
connected_roads: List[int] = field(default_factory=list)
class TrafficSystem:
"""交通系统"""
def __init__(self):
self.nodes: Dict[int, RoadNode] = {}
self.roads: Dict[int, RoadSegment] = {}
self.next_node_id = 1
self.next_road_id = 1
# 图的邻接表 (用于寻路)
self.graph: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
def add_node(self, position: Vector2) -> RoadNode:
"""添加节点"""
node = RoadNode(id=self.next_node_id, position=position)
self.nodes[self.next_node_id] = node
self.next_node_id += 1
return node
def add_road(self, start_node: int, end_node: int,
road_type: RoadType) -> RoadSegment:
"""添加道路"""
start = self.nodes[start_node]
end = self.nodes[end_node]
length = start.position.distance_to(end.position)
road = RoadSegment(
id=self.next_road_id,
start_node=start_node,
end_node=end_node,
road_type=road_type,
length=length,
capacity=road_type.value * 50 # 简化容量计算
)
self.roads[self.next_road_id] = road
# 更新节点连接
start.connected_roads.append(self.next_road_id)
end.connected_roads.append(self.next_road_id)
# 更新图 (双向)
self.graph[start_node].append((end_node, self.next_road_id))
self.graph[end_node].append((start_node, self.next_road_id))
self.next_road_id += 1
return road
def find_path(self, start_node: int, end_node: int) -> List[int]:
"""
A*寻路算法
返回道路ID列表
"""
if start_node == end_node:
return []
if start_node not in self.nodes or end_node not in self.nodes:
return []
# 启发函数: 直线距离
def heuristic(node_id: int) -> float:
node = self.nodes[node_id]
end = self.nodes[end_node]
return node.position.distance_to(end.position) / 100 # 假设最高速度100
# 优先队列: (f_score, node_id)
open_set = [(heuristic(start_node), start_node)]
came_from: Dict[int, Tuple[int, int]] = {} # node -> (prev_node, road_id)
g_score: Dict[int, float] = {start_node: 0}
while open_set:
_, current = heapq.heappop(open_set)
if current == end_node:
# 重建路径
path = []
while current in came_from:
prev_node, road_id = came_from[current]
path.append(road_id)
current = prev_node
path.reverse()
return path
for neighbor, road_id in self.graph[current]:
road = self.roads[road_id]
# 代价 = 时间 = 距离 / 速度 * 拥堵系数
travel_time = road.length / road.current_speed
tentative_g = g_score[current] + travel_time
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = (current, road_id)
g_score[neighbor] = tentative_g
f_score = tentative_g + heuristic(neighbor)
heapq.heappush(open_set, (f_score, neighbor))
return [] # 无路径
def update(self, dt: float):
"""更新交通状态"""
# 衰减车辆数 (简化模拟)
for road in self.roads.values():
road.current_vehicles = max(0, road.current_vehicles - int(dt * 10))
# ============================================================
# 资源网络系统
# ============================================================
class PowerGrid:
"""电力网格"""
def __init__(self, width: int, height: int):
self.width = width
self.height = height
# 电力层: 每个格子的电力状态
self.power_grid = [[False] * height for _ in range(width)]
# 发电厂列表
self.power_plants: List[Tuple[int, int, int]] = [] # (x, y, capacity)
# 总发电/用电量
self.total_production = 0
self.total_consumption = 0
def add_power_plant(self, x: int, y: int, capacity: int):
"""添加发电厂"""
self.power_plants.append((x, y, capacity))
def propagate_power(self, road_grid: List[List[bool]]):
"""
传播电力 (BFS洪水填充)
电力沿道路和建筑传播
"""
# 重置
self.power_grid = [[False] * self.height for _ in range(self.width)]
# 从发电厂开始BFS
queue = []
for x, y, capacity in self.power_plants:
if 0 <= x < self.width and 0 <= y < self.height:
queue.append((x, y))
self.power_grid[x][y] = True
# 4方向
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
while queue:
x, y = queue.pop(0)
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
if not self.power_grid[nx][ny]:
# 只有道路或建筑才能传电
if road_grid[nx][ny]: # 简化: 假设有道路就能传电
self.power_grid[nx][ny] = True
queue.append((nx, ny))
def has_power(self, x: int, y: int) -> bool:
"""检查某位置是否有电"""
if 0 <= x < self.width and 0 <= y < self.height:
return self.power_grid[x][y]
return False
class WaterNetwork:
"""供水网络"""
def __init__(self, width: int, height: int):
self.width = width
self.height = height
# 水压层
self.water_pressure = [[0.0] * height for _ in range(width)]
# 水泵站
self.water_pumps: List[Tuple[int, int, float]] = [] # (x, y, pressure)
def add_water_pump(self, x: int, y: int, pressure: float):
"""添加水泵站"""
self.water_pumps.append((x, y, pressure))
def propagate_water(self, pipe_grid: List[List[bool]]):
"""
传播水压 (带衰减的BFS)
"""
self.water_pressure = [[0.0] * self.height for _ in range(self.width)]
# 优先队列: (-pressure, x, y) 使用负压力实现最大堆
pq = []
for x, y, pressure in self.water_pumps:
if 0 <= x < self.width and 0 <= y < self.height:
heapq.heappush(pq, (-pressure, x, y))
self.water_pressure[x][y] = pressure
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
pressure_decay = 0.05 # 每格衰减
while pq:
neg_pressure, x, y = heapq.heappop(pq)
current_pressure = -neg_pressure
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
if pipe_grid[nx][ny]: # 有水管
new_pressure = current_pressure - pressure_decay
if new_pressure > self.water_pressure[nx][ny]:
self.water_pressure[nx][ny] = new_pressure
if new_pressure > 0.1: # 压力阈值
heapq.heappush(pq, (-new_pressure, nx, ny))
def get_pressure(self, x: int, y: int) -> float:
"""获取某位置水压"""
if 0 <= x < self.width and 0 <= y < self.height:
return self.water_pressure[x][y]
return 0.0
# ============================================================
# 需求计算系统
# ============================================================
class DemandSystem:
"""RCI需求计算"""
def __init__(self):
self.residential_demand = 0.0
self.commercial_demand = 0.0
self.industrial_demand = 0.0
def calculate_demands(self,
population: int,
jobs: int,
commercial_capacity: int,
industrial_capacity: int,
unemployment_rate: float,
pollution_level: float):
"""计算RCI需求"""
# 住宅需求
# 有工作机会时增加,失业率高时减少
job_factor = max(0, (jobs - population * 0.8) / 100)
unemployment_penalty = unemployment_rate * 2
pollution_penalty = pollution_level * 0.5
self.residential_demand = (
10 + # 基础增长
job_factor * 30 -
unemployment_penalty * 20 -
pollution_penalty * 10
)
self.residential_demand = max(-100, min(100, self.residential_demand))
# 商业需求
# 人口多时商业需求增加
consumer_demand = population * 0.5
self.commercial_demand = (
(consumer_demand - commercial_capacity) / 10
)
self.commercial_demand = max(-100, min(100, self.commercial_demand))
# 工业需求
# 商业需要货物,工业生产货物
goods_demand = commercial_capacity * 2
self.industrial_demand = (
(goods_demand - industrial_capacity) / 10
)
self.industrial_demand = max(-100, min(100, self.industrial_demand))
def get_demand_bars(self) -> Dict[str, float]:
"""获取需求条 (0-1范围)"""
return {
'R': (self.residential_demand + 100) / 200,
'C': (self.commercial_demand + 100) / 200,
'I': (self.industrial_demand + 100) / 200,
}
# ============================================================
# 游戏主类
# ============================================================
class CitySimulation:
"""城市模拟主类"""
def __init__(self, width: int = 100, height: int = 100):
self.width = width
self.height = height
# 网格
self.grid = [[GridCell(x, y) for y in range(height)] for x in range(width)]
# 子系统
self.building_manager = BuildingManager()
self.citizen_manager = CitizenManager(self.building_manager)
self.traffic_system = TrafficSystem()
self.power_grid = PowerGrid(width, height)
self.water_network = WaterNetwork(width, height)
self.demand_system = DemandSystem()
# 游戏时间
self.game_time = 0.0 # 游戏内小时
self.game_speed = 1.0
# 统计
self.population = 0
self.money = 50000
def update(self, dt: float):
"""主更新循环"""
real_dt = dt * self.game_speed
self.game_time = (self.game_time + real_dt) % 24
# 更新各系统
self._update_resources()
self._update_buildings(real_dt)
self._update_citizens(real_dt)
self._update_traffic(real_dt)
self._update_economy(real_dt)
self._update_demands()
def _update_resources(self):
"""更新资源网络"""
# 构建道路/管道网格 (简化)
road_grid = [[False] * self.height for _ in range(self.width)]
for x in range(self.width):
for y in range(self.height):
if self.grid[x][y].road_id is not None:
road_grid[x][y] = True
# 传播电力和水
self.power_grid.propagate_power(road_grid)
self.water_network.propagate_water(road_grid)
# 更新格子状态
for x in range(self.width):
for y in range(self.height):
cell = self.grid[x][y]
cell.has_power = self.power_grid.has_power(x, y)
cell.water_pressure = self.water_network.get_pressure(x, y)
cell.has_water = cell.water_pressure > 0.1
def _update_buildings(self, dt: float):
"""更新建筑"""
for building in self.building_manager.buildings.values():
self.building_manager.update_building(building, dt)
def _update_citizens(self, dt: float):
"""更新市民"""
for citizen in self.citizen_manager.citizens.values():
self.citizen_manager.update_citizen(citizen, self.game_time, dt)
self.population = len(self.citizen_manager.citizens)
def _update_traffic(self, dt: float):
"""更新交通"""
self.traffic_system.update(dt)
def _update_economy(self, dt: float):
"""更新经济"""
# 税收 (简化)
tax_rate = 0.1
hourly_income = self.population * tax_rate * dt
self.money += hourly_income
# 服务支出
service_cost = len(self.building_manager.buildings) * 0.1 * dt
self.money -= service_cost
def _update_demands(self):
"""更新RCI需求"""
# 统计
jobs = 0
commercial_capacity = 0
industrial_capacity = 0
for building in self.building_manager.buildings.values():
if building.state == BuildingState.ACTIVE:
jobs += building.max_workers
if building.zone_type == ZoneType.COMMERCIAL:
commercial_capacity += building.max_customers
elif building.zone_type == ZoneType.INDUSTRIAL:
industrial_capacity += int(building.production_rate)
# 失业率
employed = sum(1 for c in self.citizen_manager.citizens.values()
if c.workplace_id is not None)
working_age = sum(1 for c in self.citizen_manager.citizens.values()
if 18 <= c.age < 65)
unemployment = 1 - (employed / working_age) if working_age > 0 else 0
# 平均污染
total_pollution = sum(
self.grid[x][y].pollution
for x in range(self.width)
for y in range(self.height)
)
avg_pollution = total_pollution / (self.width * self.height)
self.demand_system.calculate_demands(
population=self.population,
jobs=jobs,
commercial_capacity=commercial_capacity,
industrial_capacity=industrial_capacity,
unemployment_rate=unemployment,
pollution_level=avg_pollution
)
def zone_area(self, x: int, y: int, zone_type: ZoneType):
"""规划区域"""
if 0 <= x < self.width and 0 <= y < self.height:
self.grid[x][y].zone_type = zone_type
def get_stats(self) -> dict:
"""获取统计数据"""
return {
'population': self.population,
'money': self.money,
'game_time': f"{int(self.game_time):02d}:00",
'demands': self.demand_system.get_demand_bars(),
'buildings': len(self.building_manager.buildings),
}
# ============================================================
# 演示
# ============================================================
def demo():
"""演示城市模拟"""
print("="*60)
print(" 城市模拟系统演示")
print("="*60)
# 创建城市
city = CitySimulation(50, 50)
# 添加基础设施
city.power_grid.add_power_plant(25, 25, 1000)
city.water_network.add_water_pump(20, 20, 1.0)
# 添加道路
n1 = city.traffic_system.add_node(Vector2(10, 10))
n2 = city.traffic_system.add_node(Vector2(30, 10))
n3 = city.traffic_system.add_node(Vector2(30, 30))
city.traffic_system.add_road(n1.id, n2.id, RoadType.MEDIUM)
city.traffic_system.add_road(n2.id, n3.id, RoadType.MEDIUM)
# 标记道路网格
for x in range(10, 31):
city.grid[x][10].road_id = 1
for y in range(10, 31):
city.grid[30][y].road_id = 2
# 规划区域
for x in range(5, 15):
for y in range(5, 15):
city.zone_area(x, y, ZoneType.RESIDENTIAL)
for x in range(35, 45):
for y in range(5, 15):
city.zone_area(x, y, ZoneType.INDUSTRIAL)
# 创建建筑
b1 = city.building_manager.create_building(
Vector2(10, 10), ZoneType.RESIDENTIAL, level=2
)
b1.state = BuildingState.ACTIVE
city.grid[10][10].building_id = b1.id
b2 = city.building_manager.create_building(
Vector2(35, 10), ZoneType.INDUSTRIAL, level=1
)
b2.state = BuildingState.ACTIVE
city.grid[35][10].building_id = b2.id
# 生成市民
for _ in range(10):
city.citizen_manager.spawn_citizen(b1.id)
# 为市民找工作
for citizen in city.citizen_manager.citizens.values():
city.citizen_manager.find_job(citizen)
# 模拟更新
print("\n模拟24小时...")
for hour in range(24):
city.game_time = hour
city.update(1.0)
if hour % 6 == 0:
stats = city.get_stats()
print(f"\n时间 {stats['game_time']}")
print(f" 人口: {stats['population']}")
print(f" 资金: ${stats['money']:.0f}")
print(f" 需求: R={stats['demands']['R']:.2f} "
f"C={stats['demands']['C']:.2f} "
f"I={stats['demands']['I']:.2f}")
# 寻路演示
print("\n" + "="*60)
print(" A*寻路演示")
print("="*60)
path = city.traffic_system.find_path(n1.id, n3.id)
print(f"\n从节点{n1.id}到节点{n3.id}的路径:")
print(f" 道路序列: {path}")
total_distance = sum(
city.traffic_system.roads[road_id].length
for road_id in path
)
print(f" 总距离: {total_distance:.1f}单位")
if __name__ == "__main__":
demo()
九、性能优化
┌─────────────────────────────────────────────────────────────────────────────┐
│ 性能优化策略 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【1. 空间分区】 │
│ ───────────────────────────────────────── │
│ │
│ - 四叉树/八叉树: 快速查询某区域内的建筑/Agent │
│ - 网格分区: 将地图分成chunk,只更新活跃chunk │
│ - 层次包围盒: 加速碰撞检测和范围查询 │
│ │
│ 【2. 分帧更新】 │
│ ───────────────────────────────────────── │
│ │
│ 不是每帧更新所有对象: │
│ │
│ Frame 0: 更新Agent 0-999 │
│ Frame 1: 更新Agent 1000-1999 │
│ Frame 2: 更新Agent 2000-2999 │
│ ... │
│ │
│ 【3. LOD (Level of Detail)】 │
│ ───────────────────────────────────────── │
│ │
│ 近处: 完整Agent模拟,详细建筑模型 │
│ 中距: 简化模拟,中等模型 │
│ 远处: 统计模拟,LOD模型或Billboard │
│ 超远: 只显示轮廓或不显示 │
│ │
│ 【4. 多线程】 │
│ ───────────────────────────────────────── │
│ │
│ 主线程: 渲染、输入处理 │
│ 模拟线程: Agent更新、交通计算 │
│ 资源线程: 资源网络计算 │
│ IO线程: 存档加载 │
│ │
│ 【5. 缓存与增量更新】 │
│ ───────────────────────────────────────── │
│ │
│ - 寻路结果缓存 (热门路线) │
│ - 资源网络: 只在拓扑变化时重算 │
│ - 统计数据: 定期汇总而非实时计算 │
│ │
│ 【6. 近似与简化】 │
│ ───────────────────────────────────────── │
│ │
│ - 代表性Agent: 1个Agent代表100个市民 │
│ - 概率模拟: 用概率代替完整模拟 │
│ - 聚合数据: 远处区域用统计数据代替个体 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
十、总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ 城市模拟游戏核心原理总结 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【核心系统】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 网格系统: 多层网格存储地形/区划/道路/资源 │
│ 2. Agent系统: 每个市民是独立Agent,有状态机和日程 │
│ 3. 交通系统: 图结构道路网络 + A*寻路 + 拥堵模拟 │
│ 4. 资源网络: BFS洪水填充传播电力/水 │
│ 5. 经济系统: RCI需求模型 + 税收/支出 │
│ 6. 建筑系统: 等级升级 + 废弃机制 │
│ │
│ 【关键算法】 │
│ ───────────────────────────────────────── │
│ │
│ 寻路: A* + 动态代价 (考虑拥堵) │
│ 资源: BFS洪水填充 + 带衰减传播 │
│ 覆盖: 高斯/线性衰减的服务覆盖 │
│ 需求: 多因素加权的RCI需求公式 │
│ │
│ 【性能关键】 │
│ ───────────────────────────────────────── │
│ │
│ - 代表性Agent (1:100) │
│ - 分帧更新 │
│ - LOD系统 │
│ - 多线程 │
│ - 缓存热门路径 │
│ │
│ 【死亡螺旋原因】 │
│ ───────────────────────────────────────── │
│ │
│ 废弃建筑 → 降低土地价值 → 更多废弃 → 人口流失 │
│ ↑ │
│ 服务不足 → 幸福度下降 ─┘ │
│ │
│ 解决: 及时拆除废弃建筑,保持服务覆盖 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
一句话总结:
*城市模拟游戏的核心是多系统联动:市民Agent按日程活动产生交通流量,交通系统用A+拥堵模型计算通勤,资源网络用BFS传播电力/水,经济系统根据人口/就业/服务计算RCI需求,建筑根据土地价值升级或废弃。性能关键在于代表性Agent、分帧更新和LOD。**
参考资料:
- Cities: Skylines Modding Wiki
- SimCity (2013) GDC演讲
- Agent-Based Modeling in Urban Planning
- A* Pathfinding for Beginners