第14章 游戏手感与视觉打磨
游戏手感(Juice)要素总览
┌─────────────────────────────────────────────────────────┐
│ 游戏手感要素 │
├──────────┬──────────┬──────────┬──────────┬──────────────┤
│ 视觉反馈 │ 音效反馈 │ 触觉反馈 │ 动画节奏 │ 屏幕效果 │
├──────────┼──────────┼──────────┼──────────┼──────────────┤
│ 受击闪白 │ 随机音高 │ 手柄震动 │ 预备动作 │ 屏幕震动 │
│ 粒子特效 │ 打击音效 │ 顿帧 │ 跟随动作 │ 相机 Zoom │
│ 击退动画 │ 环境音效 │ │ 弹性动画 │ 屏幕闪烁 │
│ 伤害数字 │ │ │ │ 暗角变化 │
└──────────┴──────────┴──────────┴──────────┴──────────────┘
打击感综合方案(执行顺序):
攻击命中 → 顿帧(0.05s) → 屏幕震动 → 相机Zoom脉冲
↓
受击闪白 → 伤害数字弹出 → 击退 → 粒子特效 → 音效
14.1 缓动曲线
# ease() 内置函数
var value := ease(progress, 0.5) # ease_out
# 参数:
# < 0: 先慢后快(ease_in)
# = 0: 线性
# > 0: 先快后慢(ease_out)
# 2.0: 二次方 ease_out
# -2.0: 二次方 ease_in
# Tween 的 Trans 类型
# TRANS_LINEAR 线性
# TRANS_SINE 正弦
# TRANS_QUINT 五次方
# TRANS_QUART 四次方
# TRANS_QUAD 二次方
# TRANS_EXPO 指数
# TRANS_ELASTIC 弹性
# TRANS_CUBIC 三次方
# TRANS_CIRC 圆形
# TRANS_BOUNCE 弹跳
# TRANS_BACK 回弹
# 使用
var tween := create_tween()
tween.tween_property(self, "position", target_pos, 0.5) \
.set_trans(Tween.TRANS_QUAD) \
.set_ease(Tween.EASE_OUT)
14.2 屏幕过渡效果
# 淡入淡出
extends CanvasLayer
@onready var color_rect: ColorRect = $ColorRect
func fade_in(duration: float = 0.5) -> void:
color_rect.color.a = 1.0
var tween := create_tween()
tween.tween_property(color_rect, "color:a", 0.0, duration)
func fade_out(duration: float = 0.5) -> void:
color_rect.color.a = 0.0
var tween := create_tween()
tween.tween_property(color_rect, "color:a", 1.0, duration)
await tween.finished
# 场景切换带过渡
func change_scene_with_fade(scene_path: String) -> void:
await fade_out(0.5)
get_tree().change_scene_to_file(scene_path)
await get_tree().process_frame
fade_in(0.5)
14.3 屏幕闪烁
extends CanvasLayer
@onready var flash_rect: ColorRect = $FlashRect
func flash(color: Color, duration: float = 0.1) -> void:
flash_rect.color = color
flash_rect.color.a = 0.7
var tween := create_tween()
tween.tween_property(flash_rect, "color:a", 0.0, duration)
# 受伤红闪
func damage_flash() -> void:
flash(Color(1, 0, 0, 1), 0.15)
# 治疗绿闪
func heal_flash() -> void:
flash(Color(0, 1, 0, 1), 0.2)
14.4 全屏后处理(Shader)
# 暗角效果
# 场景结构:
# CanvasLayer
# ColorRect (全屏,Mouse Filter=Ignore)
# Material: ShaderMaterial
# Shader 代码(在 ShaderMaterial 中):
# shader_type canvas_item;
# uniform float vignette_strength : hint_range(0, 1) = 0.5;
# uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
#
# void fragment() {
# vec4 color = texture(screen_texture, UV);
# float dist = distance(UV, vec2(0.5));
# float vignette = smoothstep(0.4, 0.8, dist);
# color.rgb *= 1.0 - vignette * vignette_strength;
# COLOR = color;
# }
# 代码控制强度
func set_vignette_strength(strength: float) -> void:
$VignetteRect.material.set_shader_parameter("vignette_strength", strength)
# 受伤时增强暗角
func _on_hurt() -> void:
var tween := create_tween()
tween.tween_method(set_vignette_strength, 0.8, 0.3, 0.5)
14.5 音效与视觉节奏同步
# 打击感综合方案
func on_attack_hit() -> void:
# 1. 顿帧
hitstop(0.05)
# 2. 屏幕震动
$Camera2D.shake(8.0, 0.15)
# 3. 相机 zoom 脉冲
$Camera2D.zoom_pulse(1.1, 0.1)
# 4. 屏幕白闪
$ScreenEffects.damage_flash()
# 5. 受击闪白
target.flash_white(0.1)
# 6. 伤害数字
show_damage_popup(damage, is_critical)
# 7. 击退
target.apply_knockback(direction, force)
# 8. 音效(随机音高变化)
$HitSound.pitch_scale = randf_range(0.8, 1.2)
$HitSound.play()
# 9. 粒子特效
spawn_hit_particles(hit_position)
14.6 环境反馈
# 风粒子
extends GPUParticles2D
@export var wind_direction: Vector2 = Vector2(1, 0)
@export var wind_strength: float = 100.0
func _process(delta: float) -> void:
var material: ParticleProcessMaterial = process_material
material.direction = Vector3(wind_direction.x, wind_direction.y, 0)
material.initial_velocity_min = wind_strength
material.initial_velocity_max = wind_strength * 1.5
# 物体摆动(受风影响)
extends Sprite2D
@export var sway_amount: float = 3.0
@export var sway_speed: float = 2.0
func _process(delta: float) -> void:
rotation_degrees = sin(Time.get_ticks_msec() * 0.001 * sway_speed) * sway_amount
实操练习
练习:打击感测试场景
# 创建一个测试场景,包含:
# 1. 一个可以攻击的假人
# 2. 完整的打击反馈(顿帧+震动+闪白+粒子+音效)
# 3. 伤害数字弹出
# 4. 连击系统
# 5. 屏幕后处理效果