godot-genre-shooter-fps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Genre: Shooter (FPS/TPS)

品类:射击游戏(FPS/TPS)

Gunplay feel, responsive combat, and competitive balance define shooters.
枪战手感、响应式战斗和竞技平衡性是射击游戏的核心要素。

NEVER Do (Expert Anti-Patterns)

绝对禁止的做法(专家级反模式)

Gunplay & Hit Registration

枪战与命中判定

  • NEVER use
    _process()
    for hit detection; strictly use
    _physics_process()
    to maintain frame-rate independent accuracy.
  • NEVER apply recoil to the physical weapon model; strictly apply it to Camera Rotation (kick) and Weapon Bloom (spread).
  • NEVER trust the client for hit registration in multiplayer; strictly use Server-Authoritative validation with lag compensation.
  • NEVER synchronize every bullet over the network; strictly use Client-Side Prediction and send only initial "Fire" events.
  • NEVER use
    Area3D
    or
    move_and_collide()
    for high-speed ballistics; strictly use
    PhysicsDirectSpaceState3D.intersect_ray()
    for 100x better performance.
  • NEVER forget to exclude the player's own RID from hitscan raycasts; otherwise, shots will collide instantly with the barrel.
  • NEVER use exact floating-point equality (==) for weapon cooldowns or timers; strictly use
    is_equal_approx()
    .
  • 绝对不要使用
    _process()
    进行命中检测;必须严格使用
    _physics_process()
    以保证帧率无关的判定精度。
  • 绝对不要将后坐力应用到物理武器模型上;必须严格应用到**Camera Rotation(镜头上跳)Weapon Bloom(弹道散布)**上。
  • 多人游戏中绝对不要信任客户端的命中判定结果;必须严格使用带延迟补偿的**Server-Authoritative(服务端权威)**校验机制。
  • 绝对不要通过网络同步每一颗子弹的数据;必须严格使用Client-Side Prediction(客户端预测),仅发送初始的「开火」事件。
  • 高速弹道计算绝对不要使用
    Area3D
    或者
    move_and_collide()
    ;必须严格使用
    PhysicsDirectSpaceState3D.intersect_ray()
    来获得100倍以上的性能表现。
  • 绝对不要忘记在hitscan射线检测中排除玩家自身的RID,否则子弹会立刻和枪管发生碰撞。
  • 武器冷却或计时器逻辑绝对不要使用精确浮点相等判断(==);必须严格使用
    is_equal_approx()

Performance & Polish

性能与打磨

  • NEVER use a single
    AudioStreamPlayer
    for gunfire; strictly use Layered Audio (Mechanical + Shot + Reverb Tail).
  • NEVER instantiate and
    free()
    hundreds of projectile nodes; strictly use Object Pooling or the
    RenderingServer
    .
  • NEVER use
    Sprite3D
    or
    QuadMesh
    for bullet impacts; strictly use the Decal node for surface-conforming texture projection.
  • NEVER leave decals in the scene indefinitely; strictly implement a fade-out and cleanup cycle.
  • NEVER use
    Transform3D.looking_at()
    for forward shooting vectors; strictly extract the direction from
    -transform.basis.z
    .
  • NEVER multiply velocity by
    delta
    before
    move_and_slide()
    ; the method internalizes the timestep automatically.
  • 枪声绝对不要使用单个
    AudioStreamPlayer
    播放;必须严格使用Layered Audio(分层音频)(机械音+枪声+混响尾音)。
  • 绝对不要实例化并
    free()
    上百个抛射物节点;必须严格使用**Object Pooling(对象池)**或者
    RenderingServer
  • 子弹撞击效果绝对不要使用
    Sprite3D
    或者
    QuadMesh
    ;必须严格使用Decal节点实现贴合表面的纹理投射。
  • 绝对不要让贴花永久留在场景中;必须实现渐隐和清理周期。
  • 前向射击向量计算绝对不要使用
    Transform3D.looking_at()
    ;必须严格从
    -transform.basis.z
    提取方向。
  • 在调用
    move_and_slide()
    之前绝对不要将速度乘以
    delta
    ;该方法内部会自动处理时间步长。

Input & Architecture

输入与架构

  • NEVER poll mouse motion inside
    _physics_process()
    ; strictly use
    _input()
    for zero-latency camera look.
  • NEVER accumulate mouse rotation directly onto a
    Transform3D
    ; strictly store Yaw/Pitch variables to avoid gimbal lock.
  • NEVER hardcode weapon statistics (Damage, Recoil) inside logic; strictly use Resource-based WeaponData for balancing.
  • NEVER tightly couple damage logic to specific classes; strictly use Duck-Typing (
    has_method("take_damage")
    ) for environment interactivity.
  • NEVER use standard Strings for high-frequency state identifiers; strictly use
    StringName
    (e.g.,
    &"reloading"
    ).
  • NEVER use the
    !
    (NOT) operator in AnimationTree expressions; strictly use
    is_firing == false
    .
  • NEVER connect weapon signals via string-based calls; strictly use Signal-Object syntax (
    fired.connect
    ).

  • 绝对不要在
    _physics_process()
    中轮询鼠标移动;必须严格使用
    _input()
    实现零延迟视角转动。
  • 绝对不要将鼠标旋转直接累积到
    Transform3D
    上;必须严格存储Yaw/Pitch(偏航/俯仰)变量以避免万向节锁。
  • 武器属性(伤害、后坐力)绝对不要硬编码在逻辑中;必须严格使用**Resource-based WeaponData(基于Resource的武器数据)**方便平衡性调整。
  • 伤害逻辑绝对不要和特定类强耦合;必须严格使用Duck-Typing(鸭子类型)
    has_method("take_damage")
    )实现环境交互性。
  • 高频状态标识符绝对不要使用标准字符串;必须严格使用
    StringName
    (例如
    &"reloading"
    )。
  • AnimationTree表达式中绝对不要使用
    !
    (非)运算符;必须严格使用
    is_firing == false
  • 武器信号绝对不要通过字符串调用的方式连接;必须严格使用Signal-Object syntax(信号对象语法)
    fired.connect
    )。

🛠 Expert Components (scripts/)

🛠 专家级组件(scripts/目录)

Original Expert Patterns

原创专家模式

  • advanced_weapon_controller.gd - Professional-grade weapon system with deterministic recoil, bloom, and dual hitscan/projectile modes.
  • advanced_weapon_controller.gd - 专业级武器系统,支持确定性后坐力、弹道扩散、hitscan/抛射物双模式。

Modular Components

模块化组件

  • fps_camera_look.gd - Asynchronous mouse look for zero-latency aiming using raw input.
  • hitscan_weapon_query.gd - Nodeless physics raycast pattern for instant hit registration.
  • fps_movement_logic.gd - Physics-based movement with acceleration, friction, and gravity scaling.
  • weapon_bobbing_system.gd - Procedural bobbing and sway using sine-wave oscillation.
  • bullet_decal_spawner.gd - Dynamic surface decal projection for impact effects.
  • weapon_spread_calc.gd - Gaussian/Normal distribution logic for bullet clustering.
  • server_projectile_instance.gd - High-volume visual bullets using RenderingServer RIDs.
  • weapon_state_machine.gd - Optimized state transitions for fire, reload, and idle.
  • player_anim_bridge.gd - Local velocity bridge for syncing movement with AnimationTree.
  • frame_perfect_input.gd - Buffered semi-automatic input handling to prevent dropped shots.

  • fps_camera_look.gd - 基于原始输入实现的异步鼠标视角控制,瞄准零延迟。
  • hitscan_weapon_query.gd - 无节点物理射线检测模式,实现瞬时命中判定。
  • fps_movement_logic.gd - 基于物理的移动系统,支持加速度、摩擦力和重力缩放。
  • weapon_bobbing_system.gd - 基于正弦波振荡的程序性武器摆动和晃动效果。
  • bullet_decal_spawner.gd - 动态表面贴花投射,实现撞击效果。
  • weapon_spread_calc.gd - 用于子弹聚类的高斯/正态分布逻辑。
  • server_projectile_instance.gd - 基于RenderingServer RID实现的高并发可视化子弹。
  • weapon_state_machine.gd - 优化的状态转换逻辑,支持开火、换弹、 idle状态。
  • player_anim_bridge.gd - 本地速度桥接器,用于同步移动与AnimationTree。
  • frame_perfect_input.gd - 缓冲式半自动输入处理,避免开火丢失。

Core Loop

核心循环

Engage → Aim → Fire → Kill Confirm → Acquire Next

接战 → 瞄准 → 开火 → 击杀确认 → 搜寻下一个目标

Weapon System Architecture

武器系统架构

gdscript
class_name Weapon
extends Node3D

@export_group("Stats")
@export var damage: int = 20
@export var fire_rate: float = 0.1  # Seconds between shots
@export var magazine_size: int = 30
@export var reload_time: float = 2.0
@export var range: float = 100.0

@export_group("Recoil")
@export var base_recoil: Vector2 = Vector2(0.5, 2.0)  # X, Y degrees
@export var recoil_recovery_speed: float = 5.0
@export var max_spread: float = 5.0

@export_group("Type")
@export var is_hitscan: bool = true
@export var projectile_scene: PackedScene

var current_ammo: int
var can_fire: bool = true
var current_recoil: Vector2 = Vector2.ZERO
var current_spread: float = 0.0

signal fired
signal reloaded
signal ammo_changed(current: int, max: int)

gdscript
class_name Weapon
extends Node3D

@export_group("Stats")
@export var damage: int = 20
@export var fire_rate: float = 0.1  # Seconds between shots
@export var magazine_size: int = 30
@export var reload_time: float = 2.0
@export var range: float = 100.0

@export_group("Recoil")
@export var base_recoil: Vector2 = Vector2(0.5, 2.0)  # X, Y degrees
@export var recoil_recovery_speed: float = 5.0
@export var max_spread: float = 5.0

@export_group("Type")
@export var is_hitscan: bool = true
@export var projectile_scene: PackedScene

var current_ammo: int
var can_fire: bool = true
var current_recoil: Vector2 = Vector2.ZERO
var current_spread: float = 0.0

signal fired
signal reloaded
signal ammo_changed(current: int, max: int)

Hitscan vs Projectile

Hitscan vs 抛射物

Hitscan (Instant Hit)

Hitscan(瞬时命中)

gdscript
func fire_hitscan() -> void:
    if not can_fire or current_ammo <= 0:
        return
    
    current_ammo -= 1
    ammo_changed.emit(current_ammo, magazine_size)
    
    var camera := get_viewport().get_camera_3d()
    var ray_origin := camera.global_position
    var ray_direction := -camera.global_basis.z
    
    # Apply spread
    ray_direction = apply_spread(ray_direction)
    
    var space := get_world_3d().direct_space_state
    var query := PhysicsRayQueryParameters3D.create(
        ray_origin,
        ray_origin + ray_direction * range
    )
    query.collision_mask = collision_mask
    
    var result := space.intersect_ray(query)
    if result:
        var hit_point: Vector3 = result.position
        var hit_normal: Vector3 = result.normal
        var hit_object: Object = result.collider
        
        spawn_impact_effect(hit_point, hit_normal)
        
        if hit_object.has_method("take_damage"):
            var hit_zone := determine_hit_zone(result)
            var final_damage := calculate_damage(damage, hit_zone)
            hit_object.take_damage(final_damage, hit_zone)
    
    apply_recoil()
    start_fire_cooldown()
    fired.emit()

func determine_hit_zone(result: Dictionary) -> String:
    # Use collision shape name or bone detection for hitboxes
    if "headshot" in result.collider.name.to_lower():
        return "head"
    elif "chest" in result.collider.name.to_lower():
        return "chest"
    return "body"

func calculate_damage(base: int, zone: String) -> int:
    match zone:
        "head": return int(base * 2.5)
        "chest": return int(base * 1.0)
        _: return int(base * 0.8)
gdscript
func fire_hitscan() -> void:
    if not can_fire or current_ammo <= 0:
        return
    
    current_ammo -= 1
    ammo_changed.emit(current_ammo, magazine_size)
    
    var camera := get_viewport().get_camera_3d()
    var ray_origin := camera.global_position
    var ray_direction := -camera.global_basis.z
    
    # Apply spread
    ray_direction = apply_spread(ray_direction)
    
    var space := get_world_3d().direct_space_state
    var query := PhysicsRayQueryParameters3D.create(
        ray_origin,
        ray_origin + ray_direction * range
    )
    query.collision_mask = collision_mask
    
    var result := space.intersect_ray(query)
    if result:
        var hit_point: Vector3 = result.position
        var hit_normal: Vector3 = result.normal
        var hit_object: Object = result.collider
        
        spawn_impact_effect(hit_point, hit_normal)
        
        if hit_object.has_method("take_damage"):
            var hit_zone := determine_hit_zone(result)
            var final_damage := calculate_damage(damage, hit_zone)
            hit_object.take_damage(final_damage, hit_zone)
    
    apply_recoil()
    start_fire_cooldown()
    fired.emit()

func determine_hit_zone(result: Dictionary) -> String:
    # Use collision shape name or bone detection for hitboxes
    if "headshot" in result.collider.name.to_lower():
        return "head"
    elif "chest" in result.collider.name.to_lower():
        return "chest"
    return "body"

func calculate_damage(base: int, zone: String) -> int:
    match zone:
        "head": return int(base * 2.5)
        "chest": return int(base * 1.0)
        _: return int(base * 0.8)

Projectile (Physical Bullet)

抛射物(物理子弹)

gdscript
class_name Projectile
extends CharacterBody3D

@export var speed := 100.0
@export var damage := 20
@export var gravity_affected := true
@export var lifetime := 5.0

var direction: Vector3
var shooter: Node3D

func _ready() -> void:
    await get_tree().create_timer(lifetime).timeout
    queue_free()

func _physics_process(delta: float) -> void:
    if gravity_affected:
        velocity.y -= 9.8 * delta
    
    velocity = direction * speed
    var collision := move_and_collide(velocity * delta)
    
    if collision:
        var collider := collision.get_collider()
        if collider != shooter and collider.has_method("take_damage"):
            collider.take_damage(damage)
        spawn_impact(collision.get_position(), collision.get_normal())
        queue_free()

gdscript
class_name Projectile
extends CharacterBody3D

@export var speed := 100.0
@export var damage := 20
@export var gravity_affected := true
@export var lifetime := 5.0

var direction: Vector3
var shooter: Node3D

func _ready() -> void:
    await get_tree().create_timer(lifetime).timeout
    queue_free()

func _physics_process(delta: float) -> void:
    if gravity_affected:
        velocity.y -= 9.8 * delta
    
    velocity = direction * speed
    var collision := move_and_collide(velocity * delta)
    
    if collision:
        var collider := collision.get_collider()
        if collider != shooter and collider.has_method("take_damage"):
            collider.take_damage(damage)
        spawn_impact(collision.get_position(), collision.get_normal())
        queue_free()

Recoil System

后坐力系统

Three types of recoil working together:
gdscript
class_name RecoilSystem
extends Node

var visual_recoil: Vector2 = Vector2.ZERO    # Camera kick
var pattern_offset: Vector2 = Vector2.ZERO   # Deterministic pattern
var spread_bloom: float = 0.0                # Accuracy loss

@export var recoil_pattern: Array[Vector2]   # Predefined spray pattern
var pattern_index: int = 0

func apply_recoil(weapon: Weapon) -> void:
    # 1. Visual recoil - camera kick
    visual_recoil.y += weapon.base_recoil.y * randf_range(0.8, 1.2)
    visual_recoil.x += weapon.base_recoil.x * randf_range(-1.0, 1.0)
    
    # 2. Pattern recoil - learnable spray
    if pattern_index < recoil_pattern.size():
        pattern_offset += recoil_pattern[pattern_index]
        pattern_index += 1
    
    # 3. Spread bloom - reduced accuracy
    spread_bloom = min(spread_bloom + 0.5, weapon.max_spread)

func recover_recoil(delta: float, recovery_speed: float) -> void:
    visual_recoil = visual_recoil.lerp(Vector2.ZERO, recovery_speed * delta)
    pattern_offset = pattern_offset.lerp(Vector2.ZERO, recovery_speed * delta)
    spread_bloom = lerp(spread_bloom, 0.0, recovery_speed * delta)
    
    if visual_recoil.length() < 0.01:
        pattern_index = 0  # Reset pattern

func get_spread_direction(base_direction: Vector3) -> Vector3:
    var spread_angle := deg_to_rad(spread_bloom)
    var random_offset := Vector2(
        randf_range(-spread_angle, spread_angle),
        randf_range(-spread_angle, spread_angle)
    )
    return base_direction.rotated(Vector3.UP, random_offset.x).rotated(Vector3.RIGHT, random_offset.y)

三种后坐力协同工作:
gdscript
class_name RecoilSystem
extends Node

var visual_recoil: Vector2 = Vector2.ZERO    # Camera kick
var pattern_offset: Vector2 = Vector2.ZERO   # Deterministic pattern
var spread_bloom: float = 0.0                # Accuracy loss

@export var recoil_pattern: Array[Vector2]   # Predefined spray pattern
var pattern_index: int = 0

func apply_recoil(weapon: Weapon) -> void:
    # 1. Visual recoil - camera kick
    visual_recoil.y += weapon.base_recoil.y * randf_range(0.8, 1.2)
    visual_recoil.x += weapon.base_recoil.x * randf_range(-1.0, 1.0)
    
    # 2. Pattern recoil - learnable spray
    if pattern_index < recoil_pattern.size():
        pattern_offset += recoil_pattern[pattern_index]
        pattern_index += 1
    
    # 3. Spread bloom - reduced accuracy
    spread_bloom = min(spread_bloom + 0.5, weapon.max_spread)

func recover_recoil(delta: float, recovery_speed: float) -> void:
    visual_recoil = visual_recoil.lerp(Vector2.ZERO, recovery_speed * delta)
    pattern_offset = pattern_offset.lerp(Vector2.ZERO, recovery_speed * delta)
    spread_bloom = lerp(spread_bloom, 0.0, recovery_speed * delta)
    
    if visual_recoil.length() < 0.01:
        pattern_index = 0  # Reset pattern

func get_spread_direction(base_direction: Vector3) -> Vector3:
    var spread_angle := deg_to_rad(spread_bloom)
    var random_offset := Vector2(
        randf_range(-spread_angle, spread_angle),
        randf_range(-spread_angle, spread_angle)
    )
    return base_direction.rotated(Vector3.UP, random_offset.x).rotated(Vector3.RIGHT, random_offset.y)

Aim Assist (Controller Support)

瞄准辅助(手柄支持)

gdscript
class_name AimAssist
extends Node3D

@export var assist_range := 50.0
@export var assist_angle := 15.0  # Degrees
@export var friction_strength := 0.3  # Slowdown near targets
@export var magnetism_strength := 0.1  # Pull toward targets

func apply_aim_assist(look_input: Vector2, camera: Camera3D) -> Vector2:
    var target := find_closest_target(camera)
    if not target:
        return look_input
    
    var to_target := target.global_position - camera.global_position
    var camera_forward := -camera.global_basis.z
    var angle := rad_to_deg(camera_forward.angle_to(to_target.normalized()))
    
    if angle > assist_angle:
        return look_input
    
    # Friction - slow movement near targets
    var friction := 1.0 - (friction_strength * (1.0 - angle / assist_angle))
    look_input *= friction
    
    # Magnetism - subtle pull toward target
    var target_screen_pos := camera.unproject_position(target.global_position)
    var screen_center := get_viewport().get_visible_rect().size / 2
    var pull_direction := (target_screen_pos - screen_center).normalized()
    look_input += pull_direction * magnetism_strength * (1.0 - angle / assist_angle)
    
    return look_input

func find_closest_target(camera: Camera3D) -> Node3D:
    var closest: Node3D = null
    var closest_angle := assist_angle
    
    for target in get_tree().get_nodes_in_group("enemies"):
        var to_target := target.global_position - camera.global_position
        var angle := rad_to_deg((-camera.global_basis.z).angle_to(to_target.normalized()))
        
        if angle < closest_angle and to_target.length() < assist_range:
            if has_line_of_sight(camera.global_position, target.global_position):
                closest = target
                closest_angle = angle
    
    return closest

gdscript
class_name AimAssist
extends Node3D

@export var assist_range := 50.0
@export var assist_angle := 15.0  # Degrees
@export var friction_strength := 0.3  # Slowdown near targets
@export var magnetism_strength := 0.1  # Pull toward targets

func apply_aim_assist(look_input: Vector2, camera: Camera3D) -> Vector2:
    var target := find_closest_target(camera)
    if not target:
        return look_input
    
    var to_target := target.global_position - camera.global_position
    var camera_forward := -camera.global_basis.z
    var angle := rad_to_deg(camera_forward.angle_to(to_target.normalized()))
    
    if angle > assist_angle:
        return look_input
    
    # Friction - slow movement near targets
    var friction := 1.0 - (friction_strength * (1.0 - angle / assist_angle))
    look_input *= friction
    
    # Magnetism - subtle pull toward target
    var target_screen_pos := camera.unproject_position(target.global_position)
    var screen_center := get_viewport().get_visible_rect().size / 2
    var pull_direction := (target_screen_pos - screen_center).normalized()
    look_input += pull_direction * magnetism_strength * (1.0 - angle / assist_angle)
    
    return look_input

func find_closest_target(camera: Camera3D) -> Node3D:
    var closest: Node3D = null
    var closest_angle := assist_angle
    
    for target in get_tree().get_nodes_in_group("enemies"):
        var to_target := target.global_position - camera.global_position
        var angle := rad_to_deg((-camera.global_basis.z).angle_to(to_target.normalized()))
        
        if angle < closest_angle and to_target.length() < assist_range:
            if has_line_of_sight(camera.global_position, target.global_position):
                closest = target
                closest_angle = angle
    
    return closest

Weapon Feel Polish

武器手感打磨

Camera Effects

相机效果

gdscript
func on_weapon_fired() -> void:
    # Screen shake
    camera_shake(0.1, 0.05)
    
    # FOV punch
    camera.fov += 2.0
    await get_tree().create_timer(0.05).timeout
    camera.fov -= 2.0
    
    # Muzzle flash
    muzzle_flash.visible = true
    await get_tree().create_timer(0.02).timeout
    muzzle_flash.visible = false

func on_weapon_reloaded() -> void:
    # Lock controls during reload
    can_fire = false
    can_aim = false
    
    play_animation("reload")
    await get_tree().create_timer(reload_time).timeout
    
    current_ammo = magazine_size
    can_fire = true
    can_aim = true
gdscript
func on_weapon_fired() -> void:
    # Screen shake
    camera_shake(0.1, 0.05)
    
    # FOV punch
    camera.fov += 2.0
    await get_tree().create_timer(0.05).timeout
    camera.fov -= 2.0
    
    # Muzzle flash
    muzzle_flash.visible = true
    await get_tree().create_timer(0.02).timeout
    muzzle_flash.visible = false

func on_weapon_reloaded() -> void:
    # Lock controls during reload
    can_fire = false
    can_aim = false
    
    play_animation("reload")
    await get_tree().create_timer(reload_time).timeout
    
    current_ammo = magazine_size
    can_fire = true
    can_aim = true

Audio Layering

音频分层

gdscript
@export var fire_sounds: Array[AudioStream]  # Random selection
@export var tail_sound: AudioStream           # Reverb/echo
@export var mechanical_sound: AudioStream     # Gun mechanism

func play_fire_audio() -> void:
    # Main shot
    var shot := fire_sounds.pick_random()
    fire_audio_player.stream = shot
    fire_audio_player.play()
    
    # Mechanical click
    mechanical_player.play()
    
    # Tail (delayed reverb)
    await get_tree().create_timer(0.1).timeout
    tail_player.play()

gdscript
@export var fire_sounds: Array[AudioStream]  # Random selection
@export var tail_sound: AudioStream           # Reverb/echo
@export var mechanical_sound: AudioStream     # Gun mechanism

func play_fire_audio() -> void:
    # Main shot
    var shot := fire_sounds.pick_random()
    fire_audio_player.stream = shot
    fire_audio_player.play()
    
    # Mechanical click
    mechanical_player.play()
    
    # Tail (delayed reverb)
    await get_tree().create_timer(0.1).timeout
    tail_player.play()

Weapon Selection Decision Tree

武器选择决策树

When designing weapon balance:
  • High fire rate (SMG) = Low damage per shot, rewards tracking aim
  • Low fire rate (Sniper) = High damage, rewards precision
  • Shotguns = Spread pattern (5-8 pellets), effective range <10m
  • ARs = Jack-of-all-trades, medium everything
Technical implementation:
  • Pistol/AR: Hitscan (instant feedback)
  • Rocket/Grenade: Projectile with gravity
  • S niper: Hitscan with tracer visual
设计武器平衡性时:
  • 高射速(SMG)= 每发伤害低,奖励跟枪瞄准能力
  • 低射速(狙击枪)= 高伤害,奖励精准度
  • 霰弹枪 = 散布模式(5-8发弹丸),有效射程 <10m
  • ARs = 万金油,各项属性均衡
技术实现:
  • 手枪/AR: Hitscan(即时反馈)
  • 火箭/榴弹: 带重力的抛射物
  • 狙击枪: Hitscan + 曳光弹视觉效果

Multiplayer Client Prediction Pattern

多人游戏客户端预测模式

gdscript
undefined
gdscript
undefined

CLIENT: Instant feedback, no waiting for server

CLIENT: Instant feedback, no waiting for server

func fire_client() -> void: play_effects_immediate() # Muzzle flash, recoil, audio local_hitscan_visual() # Visual blood splatter only rpc_id(1, "server_validate_shot", camera.global_transform)
func fire_client() -> void: play_effects_immediate() # Muzzle flash, recoil, audio local_hitscan_visual() # Visual blood splatter only rpc_id(1, "server_validate_shot", camera.global_transform)

SERVER: Authoritative damage

SERVER: Authoritative damage

@rpc("any_peer") func server_validate_shot(shooter_transform: Transform3D) -> void: var hit = perform_server_hitscan(shooter_transform) if hit and is_valid_shot(hit): rpc("confirm_hit", hit.victim_id, hit.damage)
@rpc("any_peer") func server_validate_shot(shooter_transform: Transform3D) -> void: var hit = perform_server_hitscan(shooter_transform) if hit and is_valid_shot(hit): rpc("confirm_hit", hit.victim_id, hit.damage)

EDGE CASE: What if client's visual hit doesn't match server?

EDGE CASE: What if client's visual hit doesn't match server?

SOLUTION: Server wins. Client shows "no reg" indicator if mismatch.

SOLUTION: Server wins. Client shows "no reg" indicator if mismatch.

undefined
undefined

Common Pitfalls & Expert Fixes

常见陷阱与专家解决方案

  • Weak bullet impact → Triple-layer audio (shot+tail+mechanical) + screen shake + blood VFX + damage number
  • Guns feel identical → Unique recoil patterns (SMG: tight vertical, AK: strong horizontal kick)
  • No skill ceiling → Learnable spray patterns (CS:GO style), not pure RNG spread
  • Controller aim frustration → Friction (0.3 slowdown near targets) + subtle 0.1 magnetism

  • 子弹打击感弱 → 三层音频(枪声+尾音+机械音)+ 屏幕震动 + 血液VFX + 伤害数字
  • 枪支手感同质化 → 独特后坐力模式(SMG: 紧凑垂直后坐力,AK: 强水平后坐力)
  • 无技能上限 → 可掌握的弹道散布模式(CS:GO风格),而非纯RNG散布
  • 手柄瞄准体验差 → 摩擦效果(靠近目标时减速0.3)+ 微妙的0.1系数吸附

Godot-Specific Tips

Godot专属技巧

  1. Raycasts: Use
    PhysicsRayQueryParameters3D
    with proper layer masks
  2. Projectiles:
    CharacterBody3D
    or
    RigidBody3D
    depending on physics needs
  3. Audio: Multiple
    AudioStreamPlayer3D
    for layered gun sounds
  4. Animations:
    AnimationTree
    for weapon state machines (idle, aim, fire, reload)
  1. Raycasts: 使用
    PhysicsRayQueryParameters3D
    搭配正确的层级掩码
  2. Projectiles: 根据物理需求选择
    CharacterBody3D
    RigidBody3D
  3. Audio: 多个
    AudioStreamPlayer3D
    实现分层枪声
  4. Animations:
    AnimationTree
    实现武器状态机(idle、瞄准、开火、换弹)

Reference

参考

  • Master Skill: godot-master
  • 主技能: godot-master