昨天我們已經能在碰撞到指定障礙產生相應效果,但是畫面中並不會顯示現在的狀態,所以這次我們透過 shader
繪製我們的特效。
這次會修改 角色
腳本。
這篇文章會使用到別人撰寫的 shader
,原始頁面:
首先開啟我們的角色場景,點擊我們角色的動畫節點 AnimatedSprite2D
在右邊的屬性面板 CanvasItem
下點擊 Texture
,接著點擊 Material
右邊的 空
,選擇 新增 MaterailShader
。
點擊材質球開啟面板,在 Shader
右邊的 空
,選擇 新shader
。
在 scripts
資料夾下新增 shaders
資料夾,將 shader
存到目錄下並點擊開啟編輯。
預設檔案如下:
shader_type canvas_item;
void fragment() {
// Place fragment code here.
}
接著我們可以撰寫屬於自己的 shader
程式,不過因為是新手,所以這裡先直接使用別人分享的原始碼,再從中理解。
shader
的種類
,分別有下述種類:
- spatial 3D 渲染。
- canvas_item 2D 渲染。
- particles 粒子系統。
- sky 天空渲染。
- fog 霧渲染。
這次我們使用的是 canvas_item
shader_type canvas_item;
uniform
關鍵字宣告參數,hint_range
提示範圍,最後設定預設值。uniform vec4 shine_color : source_color = vec4(1.0); // 顏色
uniform float cycle_speed : hint_range(0.0, 10.0, 0.1) = 1.0; // 循環速度
uniform bool full_pulse_cycle = false;
uniform int mode : hint_range(0, 2, 0) = 0; // 模式
shader 運算
,有下述種類:
- vertex 計算每個頂點顯示的最終位置。
- fragment 計算每個像素最終顯示的顏色。
- light
這次使用 fragment
調整顏色。
void fragment() {
// Place fragment code here.
}
編輯內容:
void fragment()
{
switch (mode) // 檢查模式
{
case 0: // 0 不作用
break;
case 1: // 1 脈動
{
// 使用 sin 函數能根據時間取得 -1 -> 1 -> -1 ... 的值。
float cycle = sin(TIME * cycle_speed);
// 透過調整 alpha 值實現脈動。
COLOR.rgb = mix(COLOR.rgb, shine_color.rgb, (((cycle >= 0.0) || (full_pulse_cycle)) ? abs(cycle) : 0.0) * shine_color.a);
break;
}
case 2: // 2 維持顏色
{
COLOR.rgb = mix(COLOR.rgb, shine_color.rgb, shine_color.a);
break;
}
}
}
完成後可以在屬性面板調整參數及觀看結果。
現在我們要根據角色不同狀態調整 shader
。
shader
參數並初始化。var anime_sprite = AnimatedSprite2D
func _ready():
anime_sprite = $AnimatedSprite2D
shader
參數的方法func set_shader_para( mode: int, color: Vector4 =Vector4.ZERO, speed: float = 0):
anime_sprite.material.set_shader_parameter("shine_color", color)
anime_sprite.material.set_shader_parameter("cycle_speed", speed)
anime_sprite.material.set_shader_parameter("mode", mode)
handler
中設置參數invincible:
set_shader_para(1, Vector4(0.98, 0.91, 0.66, 1), 7)
slow:
set_shader_para(1, Vector4(0.76, 0.96, 0.91, 1), 4)
stop:
set_shader_para(1, Vector4(0.97, 0.51, 0.58, 1), 1)
timeout
時關閉 shader
set_shader_para(0)
完整檔案
shader_type canvas_item;
uniform vec4 shine_color : source_color = vec4(1.0);
uniform float cycle_speed : hint_range(0.0, 10.0, 0.1) = 1.0;
uniform bool full_pulse_cycle = false;
uniform int mode : hint_range(0, 2, 0) = 0;
void fragment()
{
switch (mode)
{
case 0:
break;
case 1:
{
float cycle = sin(TIME * cycle_speed);
COLOR.rgb = mix(COLOR.rgb, shine_color.rgb, (((cycle >= 0.0) || (full_pulse_cycle)) ? abs(cycle) : 0.0) * shine_color.a);
break;
}
case 2:
{
COLOR.rgb = mix(COLOR.rgb, shine_color.rgb, shine_color.a);
break;
}
}
}
extends CharacterBody2D
# shader
var anime_sprite = AnimatedSprite2D
# CharacterBody2D
var direction:Vector2
var dragged:bool = false
var oriPos: Vector2
var player: CharacterBody2D
var player_anime: AnimatedSprite2D
# MovementUI
var movement_ui: Node2D
# Collision Handling
@export var slow_speed:float = 1
@export var base_speed:float = 5
var speed:float = base_speed
var invincible_timer:Timer
var slow_timer:Timer
var stop_timer:Timer
const DEFAULT = "DEFAULT"
var state:String = DEFAULT
# Called when the node enters the scene tree for the first time.
func _ready():
anime_sprite = $AnimatedSprite2D
movement_ui = $"../MovementUI"
movement_ui.visible = false
player = $"."
player_anime = player.get_node("AnimatedSprite2D")
invincible_timer = $"../InvincibleTimer"
invincible_timer.timeout.connect(_on_invincible_timeout)
slow_timer = $"../SlowTimer"
slow_timer.timeout.connect(_on_slow_timeout)
stop_timer = $"../StopTimer"
stop_timer.timeout.connect(_on_stop_timeout)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
if dragged:
player.position += direction*speed
handle_player_state()
func _input(event):
if event is InputEventScreenTouch:
if event.is_pressed():
oriPos = event.position
dragged = true
player_anime.play()
movement_ui.visible = true
movement_ui.position = event.position
elif event.is_released():
direction = Vector2.ZERO
dragged = false
player_anime.stop()
movement_ui.visible = false
if event is InputEventScreenDrag:
direction = (event.position - oriPos).normalized()
movement_ui.rotation = Vector2.UP.angle_to(direction)
if abs(direction.y) > abs(direction.x):
player_anime.animation = "default"
elif direction.x > 0:
player_anime.animation = "turn_right"
elif direction.x < 0:
player_anime.animation = "turn_left"
func handle_player_state():
match state:
DEFAULT:
pass
"INVINCIBLE":
handle_invincible_state()
"SLOW":
handle_slow_state()
"STOP":
handle_stop_state()
"END":
handle_end_state()
state = DEFAULT
func handle_invincible_state():
get_node("CollisionShape2D").disabled = true
speed = base_speed
set_shader_para(1, Vector4(0.98, 0.91, 0.66, 1), 7)
invincible_timer.start()
func _on_invincible_timeout():
set_shader_para(0)
get_node("CollisionShape2D").disabled = false
func handle_slow_state():
speed = slow_speed
set_shader_para(1, Vector4(0.76, 0.96, 0.91, 1), 4)
slow_timer.start()
func _on_slow_timeout():
if speed == slow_speed:
set_shader_para(0)
speed = base_speed
func handle_stop_state():
speed = 0
set_shader_para(1, Vector4(0.97, 0.51, 0.58, 1), 1)
stop_timer.start()
func _on_stop_timeout():
set_shader_para(0)
speed = base_speed
func handle_end_state():
pass
func set_shader_para( mode: int, color: Vector4 =Vector4.ZERO, speed: float = 0):
anime_sprite.material.set_shader_parameter("shine_color", color)
anime_sprite.material.set_shader_parameter("cycle_speed", speed)
anime_sprite.material.set_shader_parameter("mode", mode)
:)