316 lines
12 KiB
GDScript
316 lines
12 KiB
GDScript
class_name SurfaceView
|
|
extends Node2D
|
|
## Поверхность над убежищем: горы, холмы, деревья, облака и осадки.
|
|
##
|
|
## Текстуры — набор Kenney из `background/PNG/Flat`. Слои рисуются с разной
|
|
## скоростью относительно камеры (параллакс) и подмешивают цвет неба тем
|
|
## сильнее, чем дальше стоят, — это даёт воздушную перспективу.
|
|
##
|
|
## Как и остальной вид, всё живёт в _draw(): узлов-спрайтов нет, поэтому
|
|
## перекрасить пейзаж под время суток — это один параметр, а не обход дерева.
|
|
|
|
const MOUNTAINS := preload("res://background/PNG/Flat/pointy_mountains.png")
|
|
const HILLS_FAR := preload("res://background/PNG/Flat/hills2.png")
|
|
const HILLS_NEAR := preload("res://background/PNG/Flat/hills1.png")
|
|
## Облака — отдельными клубами, а не сплошной лентой: у ленты прямоугольник
|
|
## обрезает текстуру, и на небе видно горизонтальный шов, который вдобавок
|
|
## едет вместе с ветром.
|
|
const CLOUD_PATHS := [
|
|
"res://background/PNG/Flat/cloud1.png", "res://background/PNG/Flat/cloud2.png",
|
|
"res://background/PNG/Flat/cloud3.png", "res://background/PNG/Flat/cloud4.png",
|
|
"res://background/PNG/Flat/cloud5.png", "res://background/PNG/Flat/cloud6.png",
|
|
"res://background/PNG/Flat/cloud7.png", "res://background/PNG/Flat/cloud8.png",
|
|
"res://background/PNG/Flat/cloud9.png",
|
|
]
|
|
const CLOUD_COUNT := 30
|
|
|
|
## Ближний план берём из основного набора: там деревья и трава уже цветные.
|
|
## Набор Flat — бледные силуэты, он идёт на дальние слои, которые всё равно
|
|
## перекрашиваются под воздушную перспективу.
|
|
const TREE_PATHS := [
|
|
"res://background/PNG/tree01.png", "res://background/PNG/tree05.png",
|
|
"res://background/PNG/tree09.png", "res://background/PNG/tree12.png",
|
|
"res://background/PNG/tree18.png", "res://background/PNG/tree24.png",
|
|
"res://background/PNG/tree30.png",
|
|
]
|
|
const GRASS_PATHS := [
|
|
"res://background/PNG/grass1.png", "res://background/PNG/grass2.png",
|
|
"res://background/PNG/grass3.png", "res://background/PNG/grass4.png",
|
|
]
|
|
|
|
## Собственный цвет дальних слоёв: сами текстуры почти белые, тон задаём тут.
|
|
const MOUNTAIN_TONE := Color("#7e93ad")
|
|
const HILLS_FAR_TONE := Color("#6b8f5e")
|
|
const HILLS_NEAR_TONE := Color("#5d8248")
|
|
|
|
const LEFT := -1600.0
|
|
const RIGHT := 2900.0
|
|
## Сколько крайних столбцов текстуры отбрасываем на стыке плиток.
|
|
const EDGE_INSET := 1.0
|
|
const SCENERY_SEED := 20120401
|
|
|
|
## Полоса дёрна на уровне земли.
|
|
const TURF := Color("#4a6b3a")
|
|
const TURF_HEIGHT := 16.0
|
|
|
|
## Не ставим деревья вплотную к будке входа, чтобы её не загораживали.
|
|
const ENTRANCE_CLEAR := Vector2(-140.0, 300.0)
|
|
|
|
## Час суток и погода — сцена обновляет их вслед за часами.
|
|
var hour := 6.0
|
|
var clouds := 0.0
|
|
var precipitation := 0.0
|
|
var fog := 0.0
|
|
var wind := 0.3
|
|
var snowing := false
|
|
## Множитель хода: 0 на паузе, иначе скорость игры.
|
|
var time_scale := 1.0
|
|
|
|
var _trees: Array[Dictionary] = []
|
|
var _grass: Array[Dictionary] = []
|
|
var _cloud_puffs: Array[Dictionary] = []
|
|
var _drops: Array[Vector2] = []
|
|
var _drift := 0.0
|
|
var _fall := 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
# Повтор выключен намеренно: полосы мы кладём плитками сами, а с включённым
|
|
# повтором фильтрация на краю кадра подмешивает противоположный край
|
|
# текстуры — по небу шла тонкая линия во всю ширину.
|
|
texture_repeat = CanvasItem.TEXTURE_REPEAT_DISABLED
|
|
_scatter_scenery()
|
|
_scatter_clouds()
|
|
_scatter_drops()
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if is_zero_approx(time_scale):
|
|
return
|
|
var step := delta * time_scale
|
|
_drift += step * (18.0 + wind * 120.0)
|
|
_fall += step * (900.0 + precipitation * 900.0)
|
|
queue_redraw()
|
|
|
|
|
|
## Одним вызовом — чтобы не гонять перерисовку на каждый параметр.
|
|
func set_conditions(
|
|
hour_of_day: float, weather: WeatherSystem
|
|
) -> void:
|
|
hour = hour_of_day
|
|
clouds = weather.clouds
|
|
precipitation = weather.precipitation
|
|
fog = weather.fog
|
|
wind = weather.wind
|
|
snowing = weather.is_snowing()
|
|
queue_redraw()
|
|
|
|
|
|
func _draw() -> void:
|
|
var sky := SkyCycle.sample(hour)
|
|
var light: Color = sky["light"]
|
|
var haze: Color = sky["bottom"]
|
|
var view := _view_rect()
|
|
|
|
_draw_band(MOUNTAINS, MOUNTAIN_TONE, 1.7, 0.45, 0.25, light, haze, view)
|
|
_draw_band(HILLS_FAR, HILLS_FAR_TONE, 1.5, 0.24, 0.5, light, haze, view)
|
|
_draw_clouds(view, light)
|
|
_draw_band(HILLS_NEAR, HILLS_NEAR_TONE, 1.15, 0.08, 0.75, light, haze, view)
|
|
_draw_turf(light)
|
|
_draw_scenery(light)
|
|
_draw_fog(haze)
|
|
_draw_precipitation(view, light)
|
|
|
|
|
|
# --- Слои пейзажа -----------------------------------------------------------
|
|
|
|
## Широкая полоса, замощённая по горизонтали и стоящая на уровне земли.
|
|
## `distance` — насколько слой выцветает в цвет неба, `parallax` — насколько
|
|
## он привязан к миру (1.0 — намертво, меньше — отстаёт от камеры).
|
|
func _draw_band(
|
|
texture: Texture2D, tone: Color, scale: float, distance: float, parallax: float,
|
|
light: Color, haze: Color, view: Rect2
|
|
) -> void:
|
|
var tint := (tone * light).lerp(haze, distance)
|
|
var offset := view.get_center().x * (1.0 - parallax)
|
|
var texture_size := Vector2(texture.get_size())
|
|
|
|
# Плитку кладём вручную: draw_texture_rect(tile = true) повторяет текстуру
|
|
# в натуральную величину и по обеим осям сразу, из-за чего масштаб
|
|
# превращался в лишние ряды с полосой на стыке.
|
|
#
|
|
# Крайние столбцы пикселей у этих текстур полупрозрачные (сглаживание).
|
|
# Встык они складываются дважды и дают вертикальную линию, поэтому берём
|
|
# область с отступом в пиксель с каждой стороны и ещё на пиксель
|
|
# перекрываем соседние плитки.
|
|
var source := Rect2(
|
|
Vector2(EDGE_INSET, 0.0), Vector2(texture_size.x - EDGE_INSET * 2.0, texture_size.y)
|
|
)
|
|
var tile := Vector2(source.size.x * scale, texture_size.y * scale)
|
|
var step := tile.x - 1.0
|
|
|
|
var x := LEFT + fposmod(offset, step) - step
|
|
while x < RIGHT:
|
|
draw_texture_rect_region(texture, Rect2(Vector2(x, -tile.y), tile), source, tint)
|
|
x += step
|
|
|
|
|
|
func _draw_turf(light: Color) -> void:
|
|
draw_rect(Rect2(Vector2(LEFT, -TURF_HEIGHT), Vector2(RIGHT - LEFT, TURF_HEIGHT)), TURF * light)
|
|
|
|
|
|
func _draw_scenery(light: Color) -> void:
|
|
for tuft: Dictionary in _grass:
|
|
_draw_item(tuft, light)
|
|
for tree: Dictionary in _trees:
|
|
_draw_item(tree, light)
|
|
|
|
|
|
func _draw_item(item: Dictionary, light: Color) -> void:
|
|
var texture: Texture2D = item["texture"]
|
|
var size := Vector2(texture.get_size()) * float(item["scale"])
|
|
var origin := Vector2(float(item["x"]) - size.x * 0.5, -size.y + TURF_HEIGHT * 0.4)
|
|
draw_texture_rect(texture, Rect2(origin, size), false, light * Color(item["tint"]))
|
|
|
|
|
|
# --- Погода -----------------------------------------------------------------
|
|
|
|
func _draw_clouds(view: Rect2, light: Color) -> void:
|
|
if clouds <= 0.02:
|
|
return
|
|
|
|
var span := RIGHT - LEFT
|
|
var density := clampf(clouds, 0.0, 1.0)
|
|
# Чем плотнее облачность, тем свинцовее облака.
|
|
var tint := light.lerp(light * Color("#8ea3b8"), density)
|
|
|
|
for cloud: Dictionary in _cloud_puffs:
|
|
# У каждого облака свой порог: облачность прибавляет их числом, а не
|
|
# только непрозрачностью, иначе пасмурно выглядит как ясно, но мутнее.
|
|
var threshold: float = cloud["threshold"]
|
|
if density <= threshold:
|
|
continue
|
|
var alpha := clampf((density - threshold) * 5.0, 0.0, 1.0) * 0.9
|
|
|
|
var depth: float = cloud["depth"]
|
|
var texture: Texture2D = cloud["texture"]
|
|
var size := Vector2(texture.get_size()) * float(cloud["scale"])
|
|
var offset := view.get_center().x * (1.0 - depth) - _drift * depth
|
|
var x := LEFT + fposmod(float(cloud["x"]) + offset - LEFT, span)
|
|
|
|
draw_texture_rect(
|
|
texture, Rect2(Vector2(x, float(cloud["y"])), size), false, Color(tint, alpha)
|
|
)
|
|
|
|
|
|
## Дымка у горизонта: чем гуще, тем сильнее пейзаж растворяется в небе.
|
|
func _draw_fog(haze: Color) -> void:
|
|
if fog <= 0.01:
|
|
return
|
|
var top := -520.0
|
|
var points := PackedVector2Array([
|
|
Vector2(LEFT, top), Vector2(RIGHT, top), Vector2(RIGHT, 0.0), Vector2(LEFT, 0.0),
|
|
])
|
|
var clear := Color(haze, 0.0)
|
|
var dense := Color(haze, clampf(fog, 0.0, 1.0) * 0.85)
|
|
# Ниже нуля тумана нет — там уже разрез породы.
|
|
draw_polygon(points, PackedColorArray([clear, clear, dense, dense]))
|
|
|
|
|
|
func _draw_precipitation(view: Rect2, light: Color) -> void:
|
|
if precipitation <= 0.02:
|
|
return
|
|
|
|
# Рисуем только то, что попадает в кадр и находится над землёй.
|
|
var band := view.intersection(Rect2(LEFT, -1700.0, RIGHT - LEFT, 1700.0))
|
|
if band.size.x <= 0.0 or band.size.y <= 0.0:
|
|
return
|
|
|
|
var count := int(float(_drops.size()) * clampf(precipitation, 0.0, 1.0))
|
|
var slant := wind * 0.45
|
|
|
|
for index in count:
|
|
var drop := _drops[index]
|
|
var x := band.position.x + drop.x * band.size.x
|
|
var y := band.position.y + fposmod(drop.y * band.size.y + _fall, band.size.y)
|
|
|
|
if snowing:
|
|
draw_circle(Vector2(x, y), 3.0, Color(light, 0.75))
|
|
continue
|
|
var length := 16.0 + precipitation * 12.0
|
|
draw_line(
|
|
Vector2(x, y), Vector2(x - slant * length, y + length),
|
|
Color(light.lerp(Color("#9fc4e8"), 0.5), 0.5), 1.5
|
|
)
|
|
|
|
|
|
# --- Расстановка ------------------------------------------------------------
|
|
|
|
func _scatter_scenery() -> void:
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = SCENERY_SEED
|
|
|
|
var trees: Array[Texture2D] = []
|
|
for path: String in TREE_PATHS:
|
|
trees.append(load(path) as Texture2D)
|
|
|
|
var grass: Array[Texture2D] = []
|
|
for path: String in GRASS_PATHS:
|
|
grass.append(load(path) as Texture2D)
|
|
|
|
var x := LEFT
|
|
while x < RIGHT:
|
|
x += rng.randf_range(90.0, 260.0)
|
|
if x > ENTRANCE_CLEAR.x and x < ENTRANCE_CLEAR.y:
|
|
continue
|
|
_trees.append({
|
|
"texture": trees[rng.randi_range(0, trees.size() - 1)],
|
|
"x": x,
|
|
"scale": rng.randf_range(0.55, 1.0),
|
|
"tint": Color("#ffffff").lerp(Color("#9db98a"), rng.randf() * 0.45),
|
|
})
|
|
|
|
var tuft_x := LEFT
|
|
while tuft_x < RIGHT:
|
|
tuft_x += rng.randf_range(34.0, 130.0)
|
|
_grass.append({
|
|
"texture": grass[rng.randi_range(0, grass.size() - 1)],
|
|
"x": tuft_x,
|
|
"scale": rng.randf_range(0.9, 1.7),
|
|
"tint": Color("#ffffff"),
|
|
})
|
|
|
|
|
|
func _scatter_clouds() -> void:
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = SCENERY_SEED + 3
|
|
|
|
var textures: Array[Texture2D] = []
|
|
for path: String in CLOUD_PATHS:
|
|
textures.append(load(path) as Texture2D)
|
|
|
|
for index in CLOUD_COUNT:
|
|
_cloud_puffs.append({
|
|
"texture": textures[rng.randi_range(0, textures.size() - 1)],
|
|
"x": rng.randf_range(LEFT, RIGHT),
|
|
"y": rng.randf_range(-1020.0, -430.0),
|
|
"scale": rng.randf_range(1.3, 3.4),
|
|
# Ближние облака едут быстрее дальних.
|
|
"depth": rng.randf_range(0.25, 0.75),
|
|
"threshold": rng.randf_range(0.0, 0.9),
|
|
})
|
|
|
|
|
|
func _scatter_drops() -> void:
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = SCENERY_SEED + 7
|
|
for index in 520:
|
|
_drops.append(Vector2(rng.randf(), rng.randf()))
|
|
|
|
|
|
## Прямоугольник, который сейчас видно камерой, в координатах мира.
|
|
func _view_rect() -> Rect2:
|
|
var canvas := get_canvas_transform()
|
|
var zoom := canvas.get_scale()
|
|
return Rect2(-canvas.origin / zoom, get_viewport_rect().size / zoom)
|