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)