4. 基地建造系统
2026/4/14大约 11 分钟
基地建造系统
基地是玩家的"家",也是帕鲁的"工厂"。想象一下:你把抓来的帕鲁安排到基地里,有的采石、有的伐木、有的种田——基地就这么自动运转起来,源源不断地产出资源。这就是基地建造系统的魅力。
本章你将学到
- 自由放置建造系统(网格吸附和自由放置两种模式)
- 建筑模块的数据结构
- 帕鲁工作分配系统
- 防御设施系统
- 基地升级与扩建
基地系统总览
自由放置建造系统
建造系统有两种模式:
- 网格模式:建筑自动吸附到网格上,像搭积木一样整齐。适合建墙壁、地板、屋顶。
- 自由放置模式:建筑可以放在任意位置和角度。适合放装饰品、家具。
两种模式对比
| 特点 | 网格模式 | 自由放置模式 |
|---|---|---|
| 吸附方式 | 自动对齐到网格 | 自由放置 |
| 旋转 | 90 度固定旋转 | 任意角度旋转 |
| 适合场景 | 结构性建筑(墙、地板) | 装饰性建筑(家具、灯) |
| 碰撞检测 | 简单(网格对齐) | 复杂(需要 AABB 检测) |
| 新手友好度 | 高 | 中 |
建造管理器
C#
// BuildManager.cs
// 建造管理器——处理建筑的放置和删除
using Godot;
using System.Collections.Generic;
public partial class BuildManager : Node3D
{
[Export] public float GridSize = 2.0f; // 网格大小(米)
[Export] public float MaxPlaceDistance = 15.0f; // 最大放置距离
[Export] public float PreviewAlpha = 0.5f; // 预览透明度
private bool _isBuildMode = false;
private bool _isGridMode = true;
private Node3D _previewBuilding; // 当前预览的建筑
private string _currentBuildingId; // 当前选中的建筑ID
private Camera3D _camera;
private RayCast3D _rayCast;
// 建筑场景库:建筑ID -> 场景
private Dictionary<string, PackedScene> _buildingScenes = new();
public override void _Ready()
{
_camera = GetViewport().GetCamera3D();
}
public override void _Process(double delta)
{
if (!_isBuildMode || _previewBuilding == null) return;
UpdatePreviewPosition();
}
public override void _UnhandledInput(InputEvent @event)
{
if (!_isBuildMode) return;
// 左键放置建筑
if (@event.IsActionPressed("interact") && CanPlace())
{
PlaceBuilding();
}
// 右键取消建造
if (@event.IsActionPressed("ui_cancel"))
{
ExitBuildMode();
}
// R 键切换网格/自由模式
if (@event.IsActionPressed("rotate"))
{
_isGridMode = !_isGridMode;
GD.Print($"建造模式:{(_isGridMode ? "网格" : "自由放置")}");
}
// Q/E 键旋转建筑
if (@event.IsActionPressed("rotate_left"))
{
_previewBuilding.RotateY(-Mathf.Pi / 2); // 左转90度
}
if (@event.IsActionPressed("rotate_right"))
{
_previewBuilding.RotateY(Mathf.Pi / 2); // 右转90度
}
}
// 进入建造模式
public void EnterBuildMode(string buildingId)
{
if (!_buildingScenes.ContainsKey(buildingId)) return;
_currentBuildingId = buildingId;
_isBuildMode = true;
// 创建预览建筑(半透明)
_previewBuilding = _buildingScenes[buildingId].Instantiate<Node3D>();
// 设置半透明材质
SetPreviewAlpha(_previewBuilding, PreviewAlpha);
AddChild(_previewBuilding);
}
// 更新预览建筑的位置
private void UpdatePreviewPosition()
{
// 从摄像机向前发射射线,获取鼠标指向的地表位置
Vector2 mousePos = GetViewport().GetMousePosition();
Vector3 from = _camera.ProjectRayOrigin(mousePos);
Vector3 to = from + _camera.ProjectRayNormal(mousePos) * MaxPlaceDistance;
// 射线检测与地面碰撞
var spaceState = GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(from, to);
query.CollisionMask = 1; // 地面层
var result = spaceState.IntersectRay(query);
if (result.Count > 0)
{
Vector3 hitPos = (Vector3)result["position"];
if (_isGridMode)
{
hitPos = SnapToGrid(hitPos);
}
_previewBuilding.GlobalPosition = hitPos;
}
}
// 吸附到网格
private Vector3 SnapToGrid(Vector3 pos)
{
return new Vector3(
Mathf.Round(pos.X / GridSize) * GridSize,
pos.Y,
Mathf.Round(pos.Z / GridSize) * GridSize
);
}
// 检查能否放置(不与其他建筑重叠)
private bool CanPlace()
{
if (_previewBuilding == null) return false;
// TODO: 碰撞检测,确保不重叠
return true;
}
// 放置建筑
private void PlaceBuilding()
{
Vector3 pos = _previewBuilding.GlobalPosition;
Vector3 rot = _previewBuilding.Rotation;
// 删除预览
_previewBuilding.QueueFree();
// 创建真正的建筑
Node3D building = _buildingScenes[_currentBuildingId].Instantiate<Node3D>();
building.Position = pos;
building.Rotation = rot;
AddChild(building);
GD.Print($"放置建筑: {_currentBuildingId} 在 {pos}");
// 继续建造模式,让玩家可以连续放置
EnterBuildMode(_currentBuildingId);
}
// 退出建造模式
public void ExitBuildMode()
{
_isBuildMode = false;
if (_previewBuilding != null)
{
_previewBuilding.QueueFree();
_previewBuilding = null;
}
}
// 设置预览建筑的透明度
private void SetPreviewAlpha(Node node, float alpha)
{
if (node is MeshInstance3D mesh)
{
var mat = mesh.GetSurfaceOverrideMaterial(0);
if (mat is StandardMaterial3D stdMat)
{
stdMat.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
stdMat.AlbedoColor = new Color(stdMat.AlbedoColor, alpha);
}
}
foreach (var child in node.GetChildren())
{
SetPreviewAlpha(child, alpha);
}
}
}GDScript
# build_manager.gd
# 建造管理器——处理建筑的放置和删除
extends Node3D
## 网格大小(米)
@export var grid_size: float = 2.0
## 最大放置距离
@export var max_place_distance: float = 15.0
## 预览透明度
@export var preview_alpha: float = 0.5
var _is_build_mode: bool = false
var _is_grid_mode: bool = true
var _preview_building: Node3D # 当前预览的建筑
var _current_building_id: String # 当前选中的建筑ID
var _camera: Camera3D
# 建筑场景库:建筑ID -> 场景
var _building_scenes: Dictionary = {}
func _ready() -> void:
_camera = get_viewport().get_camera_3d()
func _process(_delta: float) -> void:
if not _is_build_mode or _preview_building == null:
return
_update_preview_position()
func _unhandled_input(event: InputEvent) -> void:
if not _is_build_mode:
return
# 左键放置建筑
if event.is_action_pressed("interact") and _can_place():
_place_building()
# 右键取消建造
if event.is_action_pressed("ui_cancel"):
exit_build_mode()
# R 键切换网格/自由模式
if event.is_action_pressed("rotate"):
_is_grid_mode = not _is_grid_mode
print("建造模式:%s" % ("网格" if _is_grid_mode else "自由放置"))
# Q/E 键旋转建筑
if event.is_action_pressed("rotate_left"):
_preview_building.rotate_y(-PI / 2) # 左转90度
if event.is_action_pressed("rotate_right"):
_preview_building.rotate_y(PI / 2) # 右转90度
## 进入建造模式
func enter_build_mode(building_id: String) -> void:
if not _building_scenes.has(building_id):
return
_current_building_id = building_id
_is_build_mode = true
# 创建预览建筑(半透明)
_preview_building = _building_scenes[building_id].instantiate() as Node3D
# 设置半透明材质
_set_preview_alpha(_preview_building, preview_alpha)
add_child(_preview_building)
## 更新预览建筑的位置
func _update_preview_position() -> void:
# 从摄像机向前发射射线,获取鼠标指向的地表位置
var mouse_pos := get_viewport().get_mouse_position()
var from := _camera.project_ray_origin(mouse_pos)
var to := from + _camera.project_ray_normal(mouse_pos) * max_place_distance
# 射线检测与地面碰撞
var space_state := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 1 # 地面层
var result := space_state.intersect_ray(query)
if result.size() > 0:
var hit_pos: Vector3 = result["position"]
if _is_grid_mode:
hit_pos = _snap_to_grid(hit_pos)
_preview_building.global_position = hit_pos
## 吸附到网格
func _snap_to_grid(pos: Vector3) -> Vector3:
return Vector3(
roundf(pos.x / grid_size) * grid_size,
pos.y,
roundf(pos.z / grid_size) * grid_size
)
## 检查能否放置
func _can_place() -> bool:
if _preview_building == null:
return false
# TODO: 碰撞检测,确保不重叠
return true
## 放置建筑
func _place_building() -> void:
var pos := _preview_building.global_position
var rot := _preview_building.rotation
# 删除预览
_preview_building.queue_free()
# 创建真正的建筑
var building := _building_scenes[_current_building_id].instantiate() as Node3D
building.position = pos
building.rotation = rot
add_child(building)
print("放置建筑: %s 在 %s" % [_current_building_id, str(pos)])
# 继续建造模式,让玩家可以连续放置
enter_build_mode(_current_building_id)
## 退出建造模式
func exit_build_mode() -> void:
_is_build_mode = false
if _preview_building != null:
_preview_building.queue_free()
_preview_building = null
## 设置预览建筑的透明度
func _set_preview_alpha(node: Node, alpha: float) -> void:
if node is MeshInstance3D:
var mesh: MeshInstance3D = node
var mat := mesh.get_surface_override_material(0)
if mat is StandardMaterial3D:
var std_mat: StandardMaterial3D = mat
std_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
std_mat.albedo_color = Color(std_mat.albedo_color, alpha)
for child in node.get_children():
_set_preview_alpha(child, alpha)建筑模块数据结构
每种建筑就像一个"零件"——有名字、有大小、有需要的材料、有功能。
建筑数据表
| 建筑名称 | 尺寸 | 所需材料 | 功能 | 解锁条件 |
|---|---|---|---|---|
| 木墙 | 2x3x0.2 | 木材x8 | 基础围墙 | 初始解锁 |
| 木地板 | 2x0.1x2 | 木材x4 | 地板 | 初始解锁 |
| 石墙 | 2x3x0.2 | 石材x10 | 更坚固的墙 | 基地等级2 |
| 工作台 | 1x1x1 | 木材x10, 石材x5 | 合成物品 | 初始解锁 |
| 帕鲁小屋 | 3x2x3 | 木材x20, 石材x15 | 帕鲁休息 | 基地等级2 |
| 储物箱 | 1x1x1 | 木材x8 | 存储物品 | 初始解锁 |
| 采石场 | 3x3x1 | 石材x20, 铁矿x5 | 帕鲁采石 | 基地等级3 |
| 伐木场 | 3x3x1 | 木材x15, 石材x10 | 帕鲁伐木 | 基地等级3 |
| 农田 | 3x1x3 | 木材x10, 种子x3 | 帕鲁种植 | 基地等级2 |
| 防御塔 | 1x3x1 | 石材x15, 铁矿x10 | 自动攻击敌人 | 基地等级4 |
| 陷阱 | 1x0.5x1 | 铁矿x5, 木材x3 | 减速敌人 | 基地等级3 |
帕鲁工作分配系统
这是基地系统的核心亮点——帕鲁可以在基地里自动工作,产出资源。
工作类型与产出
| 工作类型 | 需要的建筑 | 适合的帕鲁 | 产出资源 | 产出速度 |
|---|---|---|---|---|
| 采石 | 采石场 | 铁甲鼹等 | 石材 | 每30秒 x1 |
| 伐木 | 伐木场 | 啃木兔等 | 木材 | 每20秒 x1 |
| 种植 | 农田 | 花叶蝶等 | 食物 | 每60秒 x1 |
| 加工 | 工作台 | 任意帕鲁 | 加工品 | 按配方 |
| 搬运 | 储物箱 | 任意帕鲁 | 自动 | 产出即搬运 |
帕鲁工作分配代码
C#
// PalWorkManager.cs
// 帕鲁工作分配管理器
using Godot;
using System.Collections.Generic;
public partial class PalWorkManager : Node
{
// 工作点列表
private List<WorkStation> _workStations = new();
// 帕鲁与工作点的分配关系
private Dictionary<string, WorkStation> _assignments = new();
// 注册一个工作点
public void RegisterWorkStation(WorkStation station)
{
_workStations.Add(station);
}
// 分配帕鲁到工作点
public bool AssignPalToWork(string palId, string stationId)
{
var station = _workStations.Find(s => s.StationId == stationId);
if (station == null) return false;
if (station.IsFull) return false;
_assignments[palId] = station;
station.AddWorker(palId);
return true;
}
// 取消帕鲁的工作分配
public void UnassignPal(string palId)
{
if (_assignments.TryGetValue(palId, out var station))
{
station.RemoveWorker(palId);
_assignments.Remove(palId);
}
}
}
// 工作站点
public partial class WorkStation : Node3D
{
[Export] public string StationId { get; set; }
[Export] public string WorkType { get; set; } // "mining", "woodcutting", "farming"
[Export] public int MaxWorkers { get; set; } = 3;
[Export] public string OutputItem { get; set; } // 产出物品ID
[Export] public float WorkInterval { get; set; } = 30.0f; // 工作间隔(秒)
private HashSet<string> _workers = new();
private float _workTimer;
public bool IsFull => _workers.Count >= MaxWorkers;
public override void _Process(double delta)
{
if (_workers.Count == 0) return;
_workTimer -= (float)delta;
if (_workTimer <= 0)
{
ProduceOutput();
_workTimer = WorkInterval / _workers.Count; // 人越多越快
}
}
public void AddWorker(string palId) => _workers.Add(palId);
public void RemoveWorker(string palId) => _workers.Remove(palId);
private void ProduceOutput()
{
// 产出资源到最近的储物箱
GD.Print($"工作点 {StationId} 产出: {OutputItem}");
// TODO: 将物品添加到基地储物箱
}
}GDScript
# pal_work_manager.gd
# 帕鲁工作分配管理器
extends Node
# 工作点列表
var _work_stations: Array = []
# 帕鲁与工作点的分配关系
var _assignments: Dictionary = {}
## 注册一个工作点
func register_work_station(station: Node) -> void:
_work_stations.append(station)
## 分配帕鲁到工作点
func assign_pal_to_work(pal_id: String, station_id: String) -> bool:
var station = _find_station(station_id)
if station == null:
return false
if station.is_full:
return false
_assignments[pal_id] = station
station.add_worker(pal_id)
return true
## 取消帕鲁的工作分配
func unassign_pal(pal_id: String) -> void:
if _assignments.has(pal_id):
var station = _assignments[pal_id]
station.remove_worker(pal_id)
_assignments.erase(pal_id)
func _find_station(station_id: String) -> Node:
for station in _work_stations:
if station.station_id == station_id:
return station
return nullC#
// WorkStation.cs 已在上面的代码中定义GDScript
# work_station.gd
# 工作站点
extends Node3D
@export var station_id: String = ""
@export var work_type: String = "" # "mining", "woodcutting", "farming"
@export var max_workers: int = 3
@export var output_item: String = "" # 产出物品ID
@export var work_interval: float = 30.0 # 工作间隔(秒)
var _workers: Dictionary = {}
var _work_timer: float = 0.0
var is_full: bool:
get: return _workers.size() >= max_workers
func _process(delta: float) -> void:
if _workers.size() == 0:
return
_work_timer -= delta
if _work_timer <= 0:
_produce_output()
_work_timer = work_interval / _workers.size() # 人越多越快
func add_worker(pal_id: String) -> void:
_workers[pal_id] = true
func remove_worker(pal_id: String) -> void:
_workers.erase(pal_id)
func _produce_output() -> void:
# 产出资源到最近的储物箱
print("工作点 %s 产出: %s" % [station_id, output_item])
# TODO: 将物品添加到基地储物箱防御设施系统
夜晚会有野生帕鲁来袭击基地,所以需要防御设施。
防御建筑类型
| 防御设施 | 攻击方式 | 伤害 | 射程 | 特殊效果 |
|---|---|---|---|---|
| 石围墙 | 阻挡 | - | - | 挡住敌人前进 |
| 减速陷阱 | 范围 | 5 | 2米 | 敌人减速 50% |
| 箭塔 | 远程 | 15 | 10米 | 自动攻击最近的敌人 |
| 火焰炮塔 | 远程 | 25 | 8米 | 攻击带火属性 |
| 电击围栏 | 近战 | 10 | 1米 | 碰触的敌人麻痹 1 秒 |
基地升级与扩建
基地有等级系统,升级后解锁更多建筑和更大区域。
| 基地等级 | 所需经验 | 解锁内容 | 建造区域 |
|---|---|---|---|
| 1 级 | 0 | 基础建筑(墙、地板、工作台) | 20x20 米 |
| 2 级 | 500 | 帕鲁小屋、农田、储物箱 | 30x30 米 |
| 3 级 | 2000 | 采石场、伐木场、陷阱 | 40x40 米 |
| 4 级 | 5000 | 防御塔、加工台、高级建筑 | 50x50 米 |
| 5 级 | 15000 | 所有建筑、终极防御 | 60x60 米 |
基地经验通过帕鲁工作和完成基地任务获得。
常见问题
Q:网格模式和自由放置模式什么时候切换?
盖房子的时候用网格模式——墙壁、地板这些需要对齐。摆家具的时候切换到自由放置模式——桌子、椅子这些需要自由调整位置和角度。按 R 键随时切换。
Q:帕鲁工作会不会累?
会!帕鲁有"体力值",工作会消耗体力。体力耗尽后帕鲁会自动回小屋休息。所以你需要建足够多的帕鲁小屋,让帕鲁有地方恢复体力。勤劳性格的帕鲁体力消耗慢 30%。
Q:防御塔的弹药需要补充吗?
简化设计的话不需要——防御塔无限弹药。如果想增加游戏深度,可以设计成需要帕鲁定期给防御塔补充弹药(比如铁箭)。
Q:基地可以建多个吗?
MVP 阶段建议只做一个基地。正式版可以设计成多个基地,但每个基地需要单独的帕鲁和资源管理。玩家可以用传送点在不同基地之间快速移动。
下一步
基地建好了,接下来实现 制作与生存系统——让游戏有"活下去"的压力和目标。
