iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0
SideProject30

初探 Godot系列 第 20

[DAY 20] 特效建置 (shader, set_shader_parameter)

  • 分享至 

  • xImage
  •  

今日目標:根據狀態顯示特效


▍事前準備

昨天我們已經能在碰撞到指定障礙產生相應效果,但是畫面中並不會顯示現在的狀態,所以這次我們透過 shader 繪製我們的特效。

這次會修改 角色 腳本。

這篇文章會使用到別人撰寫的 shader,原始頁面:

https://godotshaders.com/shader/pulse-effect-godot-4/


▍出發

  • 首先開啟我們的角色場景,點擊我們角色的動畫節點 AnimatedSprite2D 在右邊的屬性面板 CanvasItem 下點擊 Texture,接著點擊 Material 右邊的 ,選擇 新增 MaterailShader
    https://ithelp.ithome.com.tw/upload/images/20231005/20162875oReQG3qZQJ.png

  • 點擊材質球開啟面板,在 Shader 右邊的 ,選擇 新shader
    https://ithelp.ithome.com.tw/upload/images/20231005/201628754otmL2Tccb.png

  • scripts 資料夾下新增 shaders 資料夾,將 shader 存到目錄下並點擊開啟編輯。
    預設檔案如下:

    shader_type canvas_item;
    
    void fragment() {
        // Place fragment code here.
    }
    
  • 接著我們可以撰寫屬於自己的 shader 程式,不過因為是新手,所以這裡先直接使用別人分享的原始碼,再從中理解。

    1. 在第一行必須先指定這個 shader種類,分別有下述種類:
    1. spatial 3D 渲染。
    2. canvas_item 2D 渲染。
    3. particles 粒子系統。
    4. sky 天空渲染。
    5. fog 霧渲染。

    這次我們使用的是 canvas_item

    shader_type canvas_item;
    
    1. 宣告參數
      這裡使用 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; // 模式
    
    1. 編輯 shader 運算,有下述種類:
    1. vertex 計算每個頂點顯示的最終位置。
    2. fragment 計算每個像素最終顯示的顏色。
    3. 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

    1. 編輯我們的角色腳本。
    2. 取得 shader 參數並初始化。
    var anime_sprite = AnimatedSprite2D
    
    func _ready():
    
        anime_sprite = $AnimatedSprite2D
    
    1. 建立修改 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)
    
    1. 根據對應的狀態 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)
    
    1. 在每個狀態 timeout 時關閉 shader
    	set_shader_para(0)
    

▍執行

這裡建立的障礙物僅作為測試,之後記得刪除回復。
Yes

▍完成

完整檔案

  1. shader
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;
		}
	}
}
  1. 角色
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)

:)


上一篇
[DAY 19] 障礙建置 Part.3 (CollisionShape2D.disabled, const)
下一篇
[DAY 21] 組合背景及障礙物 (Dictionary)
系列文
初探 Godot30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言