11. 海洋环境建设
海洋环境建设
绝地求生的地图不只有陆地——海洋是整个战场的天然边界,也是玩家登陆、游泳、驾艇的重要场所。本章教你用 Godot 做出一片真实可信的海洋环境:有起伏的波浪、有折射光线的水面、有随波漂浮的物体,以及玩家可以驾驶的快艇。
本章你将学到
- 如何用 Godot 的
ShaderMaterial制作动态水面效果 - 海洋物理:浮力系统的实现原理
- 岸边过渡:从沙滩到深海的地形处理
- 水下视觉效果:折射、模糊、颜色渐变
- 快艇系统:水上载具的物理控制
- 海上作战区域设计:港口、岛屿、礁石
为什么游戏里需要海洋?
你可以把海洋想象成一堵"有代价的墙"。陆地边界如果只是一圈山脉,玩家会觉得很假;但如果是大海,感觉就自然多了。更重要的是,海洋给游戏带来了新的玩法维度:
- 玩家可以通过游泳或驾艇悄悄绕过敌人的防线
- 海上狙击需要考虑水面波动对精准度的影响
- 落水状态下玩家移动缓慢,造成高压力时刻
- 港口和海岸线是天然的高价值物资区
海洋地形规划
一个完整的海洋区域由三层组成:
| 区域 | 水深 | 玩家状态 | 特点 |
|---|---|---|---|
| 深海区 | >5m | 只能游泳,无法跑动 | 速度慢、暴露、危险 |
| 浅滩区 | 1-5m | 涉水行走,速度降低 | 限制移动,可驾艇穿越 |
| 沙滩区 | <1m | 与陆地接近 | 登陆点,物资可能出现 |
| 陆地 | 0 | 正常移动 | 标准战斗区域 |
动态水面制作
基础原理
水面的"动感"来自两件事:顶点位移(水面起伏)和法线贴图(光线反射)。
- 顶点位移:让水面网格的每个顶点按正弦波规律上下运动,形成波浪
- 法线贴图:用一张记录"哪个方向是朝上的"的贴图,让光线在水面上产生动态反射和高光
在 Godot 中创建水面节点
首先创建水面的基础结构:
WaterSurface (MeshInstance3D)
├── Mesh: PlaneMesh (大小: 2000x2000)
└── Material: ShaderMaterial水面着色器代码
extends MeshInstance3D
## 水面控制器 - 负责更新着色器的时间参数,驱动波浪动画
class_name WaterSurface
@export var wave_speed: float = 0.5 ## 波浪速度
@export var wave_height: float = 0.3 ## 波浪高度
var shader_material: ShaderMaterial
var elapsed_time: float = 0.0
func _ready() -> void:
shader_material = mesh.surface_get_material(0) as ShaderMaterial
func _process(delta: float) -> void:
elapsed_time += delta * wave_speed
# 把当前时间传给着色器,着色器用它来计算波浪位置
shader_material.set_shader_parameter("time", elapsed_time)
shader_material.set_shader_parameter("wave_height", wave_height)using Godot;
/// <summary>
/// 水面控制器 - 负责更新着色器的时间参数,驱动波浪动画
/// </summary>
public partial class WaterSurface : MeshInstance3D
{
[Export] public float WaveSpeed = 0.5f; // 波浪速度
[Export] public float WaveHeight = 0.3f; // 波浪高度
private ShaderMaterial _shaderMaterial;
private float _elapsedTime = 0f;
public override void _Ready()
{
_shaderMaterial = (ShaderMaterial)Mesh.SurfaceGetMaterial(0);
}
public override void _Process(double delta)
{
_elapsedTime += (float)delta * WaveSpeed;
// 把当前时间传给着色器,着色器用它来计算波浪位置
_shaderMaterial.SetShaderParameter("time", _elapsedTime);
_shaderMaterial.SetShaderParameter("wave_height", WaveHeight);
}
}着色器代码(新建 water.gdshader 文件):
// 水面着色器 - 实现波浪起伏和光线反射效果
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
// === 从外部传入的参数 ===
uniform float time = 0.0; // 当前时间,用于驱动动画
uniform float wave_height = 0.3; // 波浪高度
uniform vec4 water_color : source_color = vec4(0.1, 0.3, 0.5, 0.85); // 水的颜色
uniform vec4 foam_color : source_color = vec4(0.9, 0.95, 1.0, 1.0); // 浪花颜色
uniform sampler2D normal_map_1; // 法线贴图1(控制光反射方向)
uniform sampler2D normal_map_2; // 法线贴图2(叠加更自然的波纹)
// === 顶点着色器:让水面"动起来" ===
void vertex() {
// 把顶点的 XZ 位置当作波浪计算的坐标
vec2 pos = VERTEX.xz;
// 叠加两组不同方向的正弦波,产生自然的波浪效果
// 就像两块石头扔进水里产生的涟漪叠加
float wave1 = sin(pos.x * 0.05 + time * 2.0) * wave_height;
float wave2 = sin(pos.z * 0.04 + time * 1.5 + 1.2) * wave_height * 0.7;
float wave3 = sin((pos.x + pos.z) * 0.03 + time * 1.8) * wave_height * 0.5;
// 把三组波叠加后应用到顶点的 Y 轴(上下方向)
VERTEX.y += wave1 + wave2 + wave3;
}
// === 片元着色器:控制水面看起来像什么 ===
void fragment() {
// 两张法线贴图以不同速度和方向移动,叠加出自然的水面波纹
vec2 uv1 = UV + vec2(time * 0.02, time * 0.015);
vec2 uv2 = UV + vec2(-time * 0.018, time * 0.01);
vec3 normal1 = texture(normal_map_1, uv1 * 3.0).rgb * 2.0 - 1.0;
vec3 normal2 = texture(normal_map_2, uv2 * 2.0).rgb * 2.0 - 1.0;
NORMAL_MAP = normalize(normal1 + normal2);
// 水面基础颜色
ALBEDO = water_color.rgb;
// 菲涅尔效果:从正上方看是深蓝色,从侧面看会有强烈反射
// 就像现实中你斜着看水面会看到倒影,从上面看则透到水底
float fresnel = pow(1.0 - dot(NORMAL, VIEW), 3.0);
ALBEDO = mix(water_color.rgb, vec3(1.0), fresnel * 0.3);
METALLIC = 0.0;
ROUGHNESS = 0.05; // 粗糙度低 = 反射强 = 看起来光滑
ALPHA = water_color.a;
}什么是法线贴图?
法线贴图是一张记录"每个点朝哪个方向"的图片。水面用它来欺骗光线系统:虽然模型是个平面,但光线会按照法线贴图里的方向来计算反射,使得水面看起来有细密的波纹,而不是一块镜子。
浮力系统
工作原理
浮力的本质很简单:在水面以下的部分会受到一个向上的力。在游戏里,我们检测物体有多少体积在水面以下,然后给它施加相应的向上力。
extends RigidBody3D
## 浮力组件 - 让刚体在水面上漂浮
class_name BuoyancyBody
@export var water_level: float = 0.0 ## 水面高度(世界坐标 Y)
@export var buoyancy_force: float = 20.0 ## 浮力大小(越大越难沉)
@export var water_drag: float = 0.5 ## 水的阻力(减缓水中运动)
@export var water_angular_drag: float = 1.0 ## 旋转阻力(防止物体疯狂旋转)
func _physics_process(delta: float) -> void:
var depth_submerged := water_level - global_position.y
# 只有在水面以下时才施加浮力
if depth_submerged > 0:
# 浮力:越深越大,但有上限(防止物体被弹飞)
var force := Vector3.UP * buoyancy_force * minf(depth_submerged, 2.0)
apply_central_force(force)
# 水的阻力:让物体在水中运动更慢(模拟水的粘滞感)
apply_central_force(-linear_velocity * water_drag)
apply_torque(-angular_velocity * water_angular_drag)using Godot;
/// <summary>
/// 浮力组件 - 让刚体在水面上漂浮
/// </summary>
public partial class BuoyancyBody : RigidBody3D
{
[Export] public float WaterLevel = 0f; // 水面高度(世界坐标 Y)
[Export] public float BuoyancyForce = 20f; // 浮力大小
[Export] public float WaterDrag = 0.5f; // 水的阻力
[Export] public float WaterAngularDrag = 1f; // 旋转阻力
public override void _PhysicsProcess(double delta)
{
float depthSubmerged = WaterLevel - GlobalPosition.Y;
// 只有在水面以下时才施加浮力
if (depthSubmerged > 0)
{
// 浮力:越深越大,但限制最大值防止物体被弹飞
var force = Vector3.Up * BuoyancyForce * Mathf.Min(depthSubmerged, 2f);
ApplyCentralForce(force);
// 水的阻力
ApplyCentralForce(-LinearVelocity * WaterDrag);
ApplyTorque(-AngularVelocity * WaterAngularDrag);
}
}
}玩家水中状态
玩家进入水中后,移动和战斗方式都要改变:
extends CharacterBody3D
## 玩家水中状态管理
const SWIM_SPEED = 3.0 ## 游泳速度(比跑步慢)
const SWIM_UP_SPEED = 2.0 ## 浮出水面的速度
const WATER_LEVEL = 0.0 ## 水面 Y 坐标
var is_in_water: bool = false
var is_underwater: bool = false ## 完全在水下(头没入水中)
func _physics_process(delta: float) -> void:
_check_water_state()
if is_in_water:
_handle_swimming(delta)
else:
_handle_normal_movement(delta)
func _check_water_state() -> void:
var feet_y := global_position.y
var head_y := global_position.y + 1.8 ## 假设角色身高1.8米
is_in_water = feet_y < WATER_LEVEL
is_underwater = head_y < WATER_LEVEL ## 头也在水下时才算"水下"
func _handle_swimming(delta: float) -> void:
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
# 游泳时可以控制上浮
var vertical := 0.0
if Input.is_action_pressed("jump"): ## 空格键浮上来
vertical = SWIM_UP_SPEED
elif Input.is_action_pressed("crouch"): ## 蹲键下潜
vertical = -SWIM_UP_SPEED
velocity = direction * SWIM_SPEED + Vector3.UP * vertical
move_and_slide()using Godot;
public partial class PlayerWaterState : CharacterBody3D
{
private const float SwimSpeed = 3f;
private const float SwimUpSpeed = 2f;
private const float WaterLevel = 0f;
private bool _isInWater = false;
private bool _isUnderwater = false;
public override void _PhysicsProcess(double delta)
{
CheckWaterState();
if (_isInWater)
HandleSwimming();
else
HandleNormalMovement();
}
private void CheckWaterState()
{
float feetY = GlobalPosition.Y;
float headY = GlobalPosition.Y + 1.8f;
_isInWater = feetY < WaterLevel;
_isUnderwater = headY < WaterLevel;
}
private void HandleSwimming()
{
var inputDir = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
var direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
float vertical = 0f;
if (Input.IsActionPressed("jump"))
vertical = SwimUpSpeed;
else if (Input.IsActionPressed("crouch"))
vertical = -SwimUpSpeed;
Velocity = direction * SwimSpeed + Vector3.Up * vertical;
MoveAndSlide();
}
private void HandleNormalMovement() { /* 正常陆地移动逻辑 */ }
}快艇系统
快艇是玩家在海上最快的移动方式,也是大逃杀地图中过海的关键:
extends RigidBody3D
## 快艇控制器
class_name Motorboat
@export var max_speed: float = 15.0 ## 最大速度(米/秒)
@export var acceleration: float = 8.0 ## 加速力
@export var turn_speed: float = 1.5 ## 转向速度
@export var water_level: float = 0.0 ## 水面高度
@onready var engine_audio: AudioStreamPlayer3D = $EngineAudio
var driver: Node3D = null ## 当前驾驶者
var engine_on: bool = false
func _physics_process(delta: float) -> void:
# 始终施加浮力,保持快艇在水面
_apply_buoyancy()
if driver == null:
return
if engine_on:
_handle_driving(delta)
func _apply_buoyancy() -> void:
var depth := water_level - global_position.y
if depth > 0:
apply_central_force(Vector3.UP * 50.0 * depth)
apply_central_force(-linear_velocity * 3.0) ## 水阻
func _handle_driving(delta: float) -> void:
var throttle := Input.get_action_strength("move_forward") - \
Input.get_action_strength("move_back")
var steer := Input.get_action_strength("move_left") - \
Input.get_action_strength("move_right")
# 油门:沿船头方向加速
if throttle != 0:
var forward := -global_transform.basis.z ## 船头朝向
apply_central_force(forward * throttle * acceleration)
# 转向:只有在运动时才能转向(静止的船无法原地转弯)
if steer != 0 and linear_velocity.length() > 0.5:
apply_torque(Vector3.UP * steer * turn_speed)
# 限制最大速度
if linear_velocity.length() > max_speed:
linear_velocity = linear_velocity.normalized() * max_speed
func enter_boat(player: Node3D) -> void:
driver = player
engine_on = true
engine_audio.play()
func exit_boat() -> void:
driver = null
engine_on = false
engine_audio.stop()using Godot;
/// <summary>
/// 快艇控制器
/// </summary>
public partial class Motorboat : RigidBody3D
{
[Export] public float MaxSpeed = 15f;
[Export] public float Acceleration = 8f;
[Export] public float TurnSpeed = 1.5f;
[Export] public float WaterLevel = 0f;
private Node3D _driver = null;
private bool _engineOn = false;
private AudioStreamPlayer3D _engineAudio;
public override void _Ready()
{
_engineAudio = GetNode<AudioStreamPlayer3D>("EngineAudio");
}
public override void _PhysicsProcess(double delta)
{
ApplyBuoyancy();
if (_driver == null || !_engineOn) return;
HandleDriving();
}
private void ApplyBuoyancy()
{
float depth = WaterLevel - GlobalPosition.Y;
if (depth > 0)
{
ApplyCentralForce(Vector3.Up * 50f * depth);
ApplyCentralForce(-LinearVelocity * 3f);
}
}
private void HandleDriving()
{
float throttle = Input.GetActionStrength("move_forward") -
Input.GetActionStrength("move_back");
float steer = Input.GetActionStrength("move_left") -
Input.GetActionStrength("move_right");
if (throttle != 0)
{
var forward = -GlobalTransform.Basis.Z;
ApplyCentralForce(forward * throttle * Acceleration);
}
if (steer != 0 && LinearVelocity.Length() > 0.5f)
ApplyTorque(Vector3.Up * steer * TurnSpeed);
if (LinearVelocity.Length() > MaxSpeed)
LinearVelocity = LinearVelocity.Normalized() * MaxSpeed;
}
public void EnterBoat(Node3D player)
{
_driver = player;
_engineOn = true;
_engineAudio.Play();
}
public void ExitBoat()
{
_driver = null;
_engineOn = false;
_engineAudio.Stop();
}
}海上作战区域设计
港口区域
港口是海洋地图中最重要的战略要地,同时也是物资最丰富的地方:
港口布局参考:
┌─────────────────────────────────┐
│ 海洋 │
│ ╔═══╗ │
│ ║仓库║ ← 高价值物资 │
│ ╔══╗╚═══╝ │
│ ║船║ │
│ ╚══╝ ← 快艇刷新点 │
│ ┌──────────────┐ │
│ │ 码头木板 │ │
│ └──────────────┘ │
│ ████████ 陆地 ████████ │
└─────────────────────────────────┘岛屿设计原则
| 岛屿类型 | 面积 | 物资等级 | 特点 |
|---|---|---|---|
| 主岛 | 大(主要战场) | 高 | 道路、建筑、载具 |
| 小岛 | 小 | 中-高 | 到达需要泳渡或驾艇,风险高收益也高 |
| 礁石 | 极小 | 低 | 可以用于掩体和狙击 |
设计陷阱
不要让所有优质物资都出现在岛上。如果岛屿物资太好,玩家会一直待在岛上不来大陆,导致游戏节奏停滞。建议让岛屿提供特殊装备(比如船用武器),而不是比陆地多更多普通物资。
水下视觉效果
当玩家潜入水下时,需要专门的后期处理效果:
extends Node
## 水下视觉效果管理器
class_name UnderwaterEffect
@onready var environment: Environment = $WorldEnvironment.environment
@onready var player: CharacterBody3D = $Player
## 预先保存水上的正常环境设置
var normal_fog_density: float
var normal_bg_color: Color
func _ready() -> void:
normal_fog_density = environment.fog_density
normal_bg_color = environment.background_color
func _process(_delta: float) -> void:
var is_underwater: bool = player.global_position.y < 0.0
if is_underwater:
_apply_underwater_effect()
else:
_remove_underwater_effect()
func _apply_underwater_effect() -> void:
# 水下:蓝绿色雾效,能见度降低
environment.fog_enabled = true
environment.fog_density = 0.08
environment.fog_light_color = Color(0.1, 0.3, 0.5)
environment.background_color = Color(0.05, 0.15, 0.3)
func _remove_underwater_effect() -> void:
environment.fog_density = normal_fog_density
environment.background_color = normal_bg_colorusing Godot;
/// <summary>
/// 水下视觉效果管理器
/// </summary>
public partial class UnderwaterEffect : Node
{
private Environment _environment;
private CharacterBody3D _player;
private float _normalFogDensity;
private Color _normalBgColor;
public override void _Ready()
{
_environment = GetNode<WorldEnvironment>("WorldEnvironment").Environment;
_player = GetNode<CharacterBody3D>("Player");
_normalFogDensity = _environment.FogDensity;
_normalBgColor = _environment.BackgroundColor;
}
public override void _Process(double delta)
{
bool isUnderwater = _player.GlobalPosition.Y < 0f;
if (isUnderwater)
ApplyUnderwaterEffect();
else
RemoveUnderwaterEffect();
}
private void ApplyUnderwaterEffect()
{
_environment.FogEnabled = true;
_environment.FogDensity = 0.08f;
_environment.FogLightColor = new Color(0.1f, 0.3f, 0.5f);
_environment.BackgroundColor = new Color(0.05f, 0.15f, 0.3f);
}
private void RemoveUnderwaterEffect()
{
_environment.FogDensity = _normalFogDensity;
_environment.BackgroundColor = _normalBgColor;
}
}性能优化
海洋是非常耗性能的场景元素,以下是关键优化手段:
| 优化项 | 方法 | 效果 |
|---|---|---|
| 水面细分 | 远处水面用低面数网格 | 减少 GPU 顶点计算 |
| 着色器简化 | 远处不计算菲涅尔和泡沫 | 减少 Shader 运算 |
| 浮力检测 | 只对视野内或附近物体计算 | 减少物理计算次数 |
| 水面裁剪 | 在水下不渲染水面顶面 | 减少过渡绘制 |
本章小结
海洋环境的核心是三个系统:
- 视觉水面:通过着色器实现波浪起伏和光线反射
- 物理浮力:让物体和载具自然地漂浮在水面
- 玩家水中状态:游泳、下潜、上浮的行为切换
把这三个系统组合起来,你就有了一片真实可信的海洋战场。接下来可以在港口、岛屿和礁石上设计战术据点,让玩家在陆地和海上之间做出有意义的战略选择。
下一步
海洋环境搭建完成后,进入 打磨与发布 章节,对整个游戏进行最终优化。
