简易联机实现
This commit is contained in:
+269
@@ -0,0 +1,269 @@
|
||||
extends Node
|
||||
## 联机总入口(Autoload 名:Net)
|
||||
##
|
||||
## 职责边界:只负责「选传输层 + 开局/加入/断开 + 玩家生成回收」。
|
||||
## 玩法代码不直接接触 MultiplayerPeer —— 以后要把 ENet 换成 Steam P2P,
|
||||
## 只需要改本文件的 host_game / join_game 两个函数,RPC 与 Spawner 层不用动。
|
||||
##
|
||||
## 本机双开调试(命令行):
|
||||
## godot --path <项目目录> -- --host
|
||||
## godot --path <项目目录> -- --join 127.0.0.1
|
||||
|
||||
const PORT: int = 7000
|
||||
const MAX_PLAYERS: int = 8
|
||||
const DEFAULT_ADDRESS: String = "127.0.0.1"
|
||||
## 所有玩家节点都会加入这个组,方便整批清场
|
||||
const PLAYER_GROUP: String = "net_player"
|
||||
|
||||
var _spawner: MultiplayerSpawner = null
|
||||
var _menu_root: Control = null
|
||||
var _hud_root: Control = null
|
||||
var _address_edit: LineEdit = null
|
||||
var _menu_status: Label = null
|
||||
var _hud_status: Label = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
_build_ui()
|
||||
call_deferred("_apply_cmdline")
|
||||
|
||||
|
||||
# ---------------- 对外接口 ----------------
|
||||
|
||||
## 世界根节点在 _ready 里调用,把 MultiplayerSpawner 托管给 Net。
|
||||
func bind_spawner(spawner: MultiplayerSpawner) -> void:
|
||||
_spawner = spawner
|
||||
|
||||
|
||||
## 创建主机。ENet 是客户端-服务器模型,主机同时是服务器与玩家。
|
||||
func host_game() -> void:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_server(PORT, MAX_PLAYERS)
|
||||
if err != OK:
|
||||
_set_status("创建主机失败:%s" % error_string(err))
|
||||
return
|
||||
multiplayer.multiplayer_peer = peer
|
||||
_show_game_ui()
|
||||
print("[Net] 主机已启动,端口 %d" % PORT)
|
||||
# 服务器自己也要一个角色。延后一帧,等各端场景就绪。
|
||||
call_deferred("_spawn_player", 1)
|
||||
|
||||
|
||||
## 加入主机。address 为主机在局域网内的内网 IP。
|
||||
func join_game(address: String = DEFAULT_ADDRESS) -> void:
|
||||
var target := address.strip_edges()
|
||||
if target.is_empty():
|
||||
target = DEFAULT_ADDRESS
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_client(target, PORT)
|
||||
if err != OK:
|
||||
_set_status("连接失败:%s" % error_string(err))
|
||||
return
|
||||
multiplayer.multiplayer_peer = peer
|
||||
_set_status("正在连接 %s:%d …" % [target, PORT])
|
||||
print("[Net] 正在连接 %s:%d" % [target, PORT])
|
||||
|
||||
|
||||
## 断开并清场,回到主菜单。
|
||||
func leave_game() -> void:
|
||||
multiplayer.multiplayer_peer = null
|
||||
_clear_players()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_show_menu_ui()
|
||||
_set_status("已断开")
|
||||
|
||||
|
||||
# ---------------- 网络事件 ----------------
|
||||
|
||||
func _on_peer_connected(id: int) -> void:
|
||||
# 只有服务器生成角色,客户端靠 MultiplayerSpawner 自动收到同一份。
|
||||
if multiplayer.is_server():
|
||||
call_deferred("_spawn_player", id)
|
||||
|
||||
|
||||
func _on_peer_disconnected(id: int) -> void:
|
||||
if multiplayer.is_server():
|
||||
_despawn_player(id)
|
||||
|
||||
|
||||
func _on_connected_to_server() -> void:
|
||||
_show_game_ui()
|
||||
print("[Net] 已连接,等待主机生成角色")
|
||||
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
multiplayer.multiplayer_peer = null
|
||||
_set_status("连接失败:主机无响应")
|
||||
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
multiplayer.multiplayer_peer = null
|
||||
_clear_players()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_show_menu_ui()
|
||||
_set_status("主机已关闭")
|
||||
|
||||
|
||||
# ---------------- 玩家生成与回收 ----------------
|
||||
|
||||
func _spawn_player(peer_id: int) -> void:
|
||||
if _spawner == null or not multiplayer.is_server():
|
||||
return
|
||||
var parent := _spawner.get_node_or_null(_spawner.spawn_path)
|
||||
if parent == null or parent.has_node(str(peer_id)):
|
||||
return
|
||||
_spawner.spawn(peer_id)
|
||||
|
||||
|
||||
func _despawn_player(peer_id: int) -> void:
|
||||
if _spawner == null:
|
||||
return
|
||||
var parent := _spawner.get_node_or_null(_spawner.spawn_path)
|
||||
if parent == null:
|
||||
return
|
||||
var node := parent.get_node_or_null(str(peer_id))
|
||||
if node != null:
|
||||
node.queue_free()
|
||||
|
||||
|
||||
func _clear_players() -> void:
|
||||
for node in get_tree().get_nodes_in_group(PLAYER_GROUP):
|
||||
node.queue_free()
|
||||
|
||||
|
||||
# ---------------- 命令行调试入口 ----------------
|
||||
|
||||
func _apply_cmdline() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.has("--host"):
|
||||
host_game()
|
||||
elif args.has("--join"):
|
||||
var index := args.find("--join")
|
||||
var address := DEFAULT_ADDRESS
|
||||
if index + 1 < args.size():
|
||||
address = args[index + 1]
|
||||
join_game(address)
|
||||
|
||||
|
||||
# ---------------- 界面 ----------------
|
||||
|
||||
func _build_ui() -> void:
|
||||
var menu_layer := CanvasLayer.new()
|
||||
menu_layer.name = "NetMenu"
|
||||
menu_layer.layer = 20
|
||||
add_child(menu_layer)
|
||||
_menu_root = _build_menu()
|
||||
menu_layer.add_child(_menu_root)
|
||||
|
||||
var hud_layer := CanvasLayer.new()
|
||||
hud_layer.name = "NetHud"
|
||||
hud_layer.layer = 19
|
||||
add_child(hud_layer)
|
||||
_hud_root = _build_hud()
|
||||
_hud_root.visible = false
|
||||
hud_layer.add_child(_hud_root)
|
||||
|
||||
|
||||
func _build_menu() -> Control:
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
center.add_child(panel)
|
||||
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
panel.add_child(box)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "联机测试"
|
||||
title.add_theme_font_size_override("font_size", 20)
|
||||
box.add_child(title)
|
||||
|
||||
_address_edit = LineEdit.new()
|
||||
_address_edit.text = DEFAULT_ADDRESS
|
||||
_address_edit.custom_minimum_size = Vector2(240, 0)
|
||||
box.add_child(_address_edit)
|
||||
|
||||
var host_button := Button.new()
|
||||
host_button.text = "创建主机"
|
||||
host_button.pressed.connect(host_game)
|
||||
box.add_child(host_button)
|
||||
|
||||
var join_button := Button.new()
|
||||
join_button.text = "加入主机"
|
||||
join_button.pressed.connect(_on_join_pressed)
|
||||
box.add_child(join_button)
|
||||
|
||||
_menu_status = Label.new()
|
||||
_menu_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_menu_status.custom_minimum_size = Vector2(240, 0)
|
||||
box.add_child(_menu_status)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
hint.custom_minimum_size = Vector2(240, 0)
|
||||
hint.modulate = Color(1, 1, 1, 0.7)
|
||||
hint.text = "局域网内填主机的内网 IP;本机双开测试填 %s" % DEFAULT_ADDRESS
|
||||
box.add_child(hint)
|
||||
|
||||
return center
|
||||
|
||||
|
||||
func _build_hud() -> Control:
|
||||
var layer_root := Control.new()
|
||||
layer_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
layer_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var box := VBoxContainer.new()
|
||||
box.position = Vector2(16, 12)
|
||||
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
layer_root.add_child(box)
|
||||
|
||||
_hud_status = Label.new()
|
||||
box.add_child(_hud_status)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.modulate = Color(1, 1, 1, 0.7)
|
||||
hint.text = "F10 断开 · Esc 释放鼠标"
|
||||
box.add_child(hint)
|
||||
|
||||
return layer_root
|
||||
|
||||
|
||||
func _on_join_pressed() -> void:
|
||||
join_game(_address_edit.text)
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and not event.echo \
|
||||
and event.keycode == KEY_F10 and multiplayer.multiplayer_peer != null:
|
||||
leave_game()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _show_game_ui() -> void:
|
||||
_menu_root.visible = false
|
||||
_hud_root.visible = true
|
||||
_update_hud()
|
||||
|
||||
|
||||
func _show_menu_ui() -> void:
|
||||
_menu_root.visible = true
|
||||
_hud_root.visible = false
|
||||
|
||||
|
||||
func _set_status(text: String) -> void:
|
||||
if _menu_status != null:
|
||||
_menu_status.text = text
|
||||
|
||||
|
||||
func _update_hud() -> void:
|
||||
var role := "主机" if multiplayer.is_server() else "客户端"
|
||||
var players := multiplayer.get_peers().size() + 1
|
||||
_hud_status.text = "联机中 · %s · 玩家 %d" % [role, players]
|
||||
@@ -0,0 +1 @@
|
||||
uid://tqratkoyv2up
|
||||
@@ -45,9 +45,21 @@ var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_grav
|
||||
var _yaw: float = 0.0
|
||||
var _pitch: float = 0.0
|
||||
var _jump_buffer: int = 0
|
||||
## 本节点是否由本机玩家操控。由世界节点在生成时设置 authority 决定,见 world/main.gd。
|
||||
var _is_local: bool = true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_is_local = is_multiplayer_authority()
|
||||
if not _is_local:
|
||||
# 远端玩家只是一具「回放身体」:
|
||||
# 拆掉相机 —— 否则每多一个玩家就多一台 Camera3D 互相抢渲染;
|
||||
# 关掉输入与物理 —— 否则与权威端双重模拟,表现为抖动、漂移、穿地。
|
||||
# 它的 position 与 Body.rotation 由子节点 Sync 同步过来。
|
||||
camera_rig.queue_free()
|
||||
set_physics_process(false)
|
||||
set_process_unhandled_input(false)
|
||||
return
|
||||
_yaw = camera_rig.rotation.y
|
||||
_pitch = clampf(spring_arm.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg))
|
||||
spring_arm.rotation.x = _pitch
|
||||
@@ -77,6 +89,8 @@ func set_mouse_captured(captured: bool) -> void:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not _is_local:
|
||||
return
|
||||
# --- 重力 ---
|
||||
if is_on_floor():
|
||||
if velocity.y < 0.0:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://bekuuw153mv03"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://f5ptp3is4agf" path="res://player/player.gd" id="1_oul6g"]
|
||||
[ext_resource type="SceneReplicationConfig" path="res://player/player_sync.tres" id="2_rkbax"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_rkbax"]
|
||||
radius = 0.4
|
||||
@@ -39,3 +40,6 @@ fov = 82.2
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
|
||||
mesh = SubResource("CapsuleMesh_g1dw6")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_yw30f")
|
||||
|
||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=1979575210]
|
||||
replication_config = ExtResource("2_rkbax")
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="SceneReplicationConfig" format=3]
|
||||
|
||||
[resource]
|
||||
properties/0/path = NodePath(".:position")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath("Body:rotation")
|
||||
properties/1/spawn = true
|
||||
properties/1/replication_mode = 1
|
||||
@@ -19,6 +19,7 @@ config/icon="res://icon.svg"
|
||||
|
||||
Steamworks="*uid://w087kpi6dipk"
|
||||
_mcp_game_helper="*res://addons/godot_ai/runtime/game_helper.gd"
|
||||
Net="*res://autoload/net.gd"
|
||||
|
||||
[debug]
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
@tool
|
||||
extends McpTestSuite
|
||||
## 回归测试:Player 的同步配置必须真的被解析成条目。
|
||||
##
|
||||
## 这是一处典型的「静默失效」:如果 .tres 里的键名写错,或 replication_mode
|
||||
## 落到 NEVER,远端玩家会僵在原地不动,而且不打任何错误。用测试钉住它。
|
||||
|
||||
const CONFIG_PATH := "res://player/player_sync.tres"
|
||||
const PLAYER_SCENE_PATH := "res://player/player.tscn"
|
||||
|
||||
|
||||
func suite_name() -> String:
|
||||
return "net"
|
||||
|
||||
|
||||
func test_config_has_expected_paths() -> void:
|
||||
var config := load(CONFIG_PATH) as SceneReplicationConfig
|
||||
assert_true(config != null, "player_sync.tres 未能加载为 SceneReplicationConfig")
|
||||
if config == null:
|
||||
return
|
||||
var paths := config.get_properties()
|
||||
assert_eq(paths.size(), 2, "同步字段数量不对:%s" % [paths])
|
||||
assert_true(config.has_property(^".:position"), "缺少 .:position")
|
||||
assert_true(config.has_property(^"Body:rotation"), "缺少 Body:rotation")
|
||||
|
||||
|
||||
func test_config_replication_mode_is_always() -> void:
|
||||
var config := load(CONFIG_PATH) as SceneReplicationConfig
|
||||
assert_true(config != null, "player_sync.tres 未能加载")
|
||||
if config == null:
|
||||
return
|
||||
var paths := config.get_properties()
|
||||
assert_gt(paths.size(), 0, "同步字段为空,远端玩家不会移动")
|
||||
for path in paths:
|
||||
assert_eq(
|
||||
config.property_get_replication_mode(path),
|
||||
SceneReplicationConfig.REPLICATION_MODE_ALWAYS,
|
||||
"%s 的 replication_mode 不是 ALWAYS" % path
|
||||
)
|
||||
|
||||
|
||||
func test_player_scene_wires_synchronizer() -> void:
|
||||
var packed := load(PLAYER_SCENE_PATH) as PackedScene
|
||||
assert_true(packed != null, "player.tscn 未能加载")
|
||||
if packed == null:
|
||||
return
|
||||
var player := packed.instantiate()
|
||||
track(player)
|
||||
var sync := player.get_node_or_null("Sync") as MultiplayerSynchronizer
|
||||
assert_true(sync != null, "Player 下缺少 Sync 节点")
|
||||
if sync == null:
|
||||
return
|
||||
assert_true(sync.replication_config != null, "Sync 没有绑定 SceneReplicationConfig")
|
||||
assert_eq(sync.root_path, ^"..", "Sync.root_path 应为 .. (相对 Player)")
|
||||
@@ -0,0 +1 @@
|
||||
uid://nvx3ojicfxh5
|
||||
@@ -0,0 +1,35 @@
|
||||
extends Node3D
|
||||
## 世界根节点。
|
||||
##
|
||||
## 只做一件事:把玩家场景注册给 MultiplayerSpawner,并把 spawner 托管给 Net 单例。
|
||||
## 玩家的生成/回收时机完全由 Net 决定,本文件不主动调用 spawn。
|
||||
|
||||
const PLAYER_SCENE: PackedScene = preload("res://player/player.tscn")
|
||||
const PLAYER_GROUP: String = "net_player"
|
||||
## 用节点路径而不是全局标识符访问单例:
|
||||
## autoload 的全局名只在引擎启动时注册,这样写不依赖注册时机。
|
||||
const NET_PATH: String = "/root/Net"
|
||||
|
||||
@onready var spawner: MultiplayerSpawner = $MultiplayerSpawner
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
spawner.spawn_function = _create_player
|
||||
var net := get_node_or_null(NET_PATH)
|
||||
if net != null:
|
||||
net.bind_spawner(spawner)
|
||||
else:
|
||||
push_error("Net 单例未注册,联机不可用。请确认 project.godot 的 [autoload] 里有 Net")
|
||||
|
||||
|
||||
## MultiplayerSpawner 会在**每一端**都调用这个函数,因此 authority 必须在这里设置:
|
||||
## 这样「谁是本地玩家」在各端结论一致,玩家脚本里的 is_multiplayer_authority() 才可靠。
|
||||
## recursive=true 很关键 —— 节点 authority 不自动继承,Sync 子节点必须一起设。
|
||||
func _create_player(peer_id: int) -> Node:
|
||||
var player := PLAYER_SCENE.instantiate()
|
||||
player.name = str(peer_id)
|
||||
player.add_to_group(PLAYER_GROUP)
|
||||
player.set_multiplayer_authority(peer_id, true)
|
||||
var here := multiplayer.get_unique_id()
|
||||
print("[Net] 本端 %d 生成玩家 %d%s" % [here, peer_id, "(本地操控)" if peer_id == here else "(远端回放)"])
|
||||
return player
|
||||
@@ -0,0 +1 @@
|
||||
uid://11rbhffe5dhv
|
||||
+6
-5
@@ -1,7 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://bp8l1uwgshc86"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bekuuw153mv03" path="res://player/player.tscn" id="1_gjxf3"]
|
||||
[ext_resource type="Sky" path="res://world/sky/cartoon_sky.tres" id="2_cartoon"]
|
||||
[ext_resource type="Script" uid="uid://11rbhffe5dhv" path="res://world/main.gd" id="1_hyim0"]
|
||||
[ext_resource type="Sky" uid="uid://bi632suyifms5" path="res://world/sky/cartoon_sky.tres" id="2_cartoon"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_hyim0"]
|
||||
size = Vector3(40, 0.5, 40)
|
||||
@@ -22,6 +22,7 @@ ambient_light_source = 3
|
||||
reflected_light_source = 2
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=58155864]
|
||||
script = ExtResource("1_hyim0")
|
||||
|
||||
[node name="Ground" type="StaticBody3D" parent="." unique_id=305091665]
|
||||
|
||||
@@ -40,8 +41,8 @@ shadow_enabled = true
|
||||
directional_shadow_blend_splits = true
|
||||
directional_shadow_max_distance = 40.0
|
||||
|
||||
[node name="Player" parent="." unique_id=869687860 instance=ExtResource("1_gjxf3")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)
|
||||
|
||||
[node name="Environment" type="WorldEnvironment" parent="." unique_id=794103784]
|
||||
environment = SubResource("Environment_gu74f")
|
||||
|
||||
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=1410622657]
|
||||
spawn_path = NodePath("..")
|
||||
|
||||
@@ -4,6 +4,45 @@
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_cartoon"]
|
||||
shader = ExtResource("1_cartoon")
|
||||
shader_parameter/zenith_color = Color(0.09, 0.3, 0.78, 1)
|
||||
shader_parameter/mid_color = Color(0.28, 0.6, 0.95, 1)
|
||||
shader_parameter/horizon_color = Color(0.78, 0.88, 1, 1)
|
||||
shader_parameter/ground_color = Color(0.29, 0.329, 0.384, 1)
|
||||
shader_parameter/gradient_bias = 1.4
|
||||
shader_parameter/band_count = 0.0
|
||||
shader_parameter/band_sharpness = 0.9
|
||||
shader_parameter/follow_scene_light = 1.0
|
||||
shader_parameter/sun_direction = Vector3(0.35, 0.55, 0.35)
|
||||
shader_parameter/sun_core_color = Color(1, 0.976, 0.878, 1)
|
||||
shader_parameter/sun_halo_color = Color(1, 0.827, 0.478, 1)
|
||||
shader_parameter/sun_disk_size = 0.035
|
||||
shader_parameter/sun_disk_softness = 0.2
|
||||
shader_parameter/sun_halo_size = 0.55
|
||||
shader_parameter/sun_halo_strength = 0.65
|
||||
shader_parameter/sun_bloom_strength = 0.35
|
||||
shader_parameter/sun_intensity = 1.7
|
||||
shader_parameter/star_amount = 0.0
|
||||
shader_parameter/star_density = 60.0
|
||||
shader_parameter/star_size = 0.05
|
||||
shader_parameter/star_twinkle = 0.65
|
||||
shader_parameter/star_color = Color(0.85, 0.9, 1, 1)
|
||||
shader_parameter/cloud_color = Color(1, 1, 1, 1)
|
||||
shader_parameter/cloud_shadow_color = Color(0.45, 0.62, 0.88, 1)
|
||||
shader_parameter/cloud_rim_color = Color(1, 0.93, 0.76, 1)
|
||||
shader_parameter/cloud_coverage = 0.55
|
||||
shader_parameter/cloud_scale = 4.0
|
||||
shader_parameter/cloud_plane_offset = 0.6
|
||||
shader_parameter/cloud_horizon_fade = 0.08
|
||||
shader_parameter/cloud_edge = 0.16
|
||||
shader_parameter/cloud_warp = 0.3
|
||||
shader_parameter/cloud_speed = 0.01
|
||||
shader_parameter/cloud_direction = Vector2(1, 0.25)
|
||||
shader_parameter/cloud_shading = 0.55
|
||||
shader_parameter/cloud_rim = 0.4
|
||||
shader_parameter/cloud_opacity = 1.0
|
||||
shader_parameter/haze_color = Color(0.8, 0.89, 1, 1)
|
||||
shader_parameter/haze_strength = 0.35
|
||||
shader_parameter/haze_height = 0.09
|
||||
|
||||
[resource]
|
||||
sky_material = SubResource("ShaderMaterial_cartoon")
|
||||
|
||||
Reference in New Issue
Block a user