Skip to content

Commit

Permalink
feat: adds initial camera shake implementation (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
OctoD committed Jun 28, 2023
1 parent e28b5e7 commit 5aeb5ae
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 5 deletions.
193 changes: 193 additions & 0 deletions addons/godot_gameplay_systems/camera_shake/nodes/camera_shake.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
class_name CameraShake extends Node


## Emitted when the shake ends
signal shake_ended()
## Emitted when the shake starts
signal shake_started()
## Emitted when a shake occurs
signal shaken(remaining_times: int)


enum CameraContext {
CAMERA_2D,
CAMERA_3D,
}


@export_category("Camera shake")
## The path to the camera to shake
@export_node_path("Camera2D", "Camera3D") var camera_path: NodePath = NodePath()

@export_group("Minimum values", "min_")
## The minimum strength appliable to the shake
@export var min_strength: float = 0.0
## How many seconds the camera will be shaked (min)
@export var min_duration: float = 0.0
## How many times per second the camera will be shaked (min)
@export var min_frequency: int = 0

@export_group("Maximum values", "max_")
## The maximum strength appliable to the shake
@export var max_strength: float = 50.0
## How many seconds the camera will be shaked (max)
@export var max_duration: float = 5.0
## How many times per second the camera will be shaked (max)
@export var max_frequency: int = 50

## The camera 2d/3d node
var camera: Node
## The camera context
var camera_context: CameraContext
## Current applied duration
var duration: float = 0.0:
get:
return duration
set(value):
duration = clampf(value, min_duration, max_duration)
## Current applied frequency
var frequency: int = 0:
get:
return frequency
set(value):
frequency = clampf(value, min_frequency, max_frequency)
## Previous used [Tween]. Is [code]null[/code] if the [member CameraShake.camera] never shook or the previous [Tween] finished it's own job.
var previous_tween: Tween
## Current applied strength
var strength: float = 0.0:
get:
return strength
set(value):
strength = clampf(value, min_strength, max_strength)
## Time remaining before the shake effect ends
var time_remaining: float = 0.0:
get:
return duration / float(frequency)
## Returns the number of tweens to operate
var tweens_range: Array:
get:
return range(0, duration * frequency)

## Applies the shake
func _apply_shake() -> void:
if camera_context == CameraContext.CAMERA_2D:
_apply_shake_2d()
elif camera_context == CameraContext.CAMERA_3D:
_apply_shake_3d()


## Applies the shake using 2D context
func _apply_shake_2d() -> void:
var _camera = camera as Camera2D

assert(_camera != null, "Camera (2D) should not be null at this point, check your code before shaking the camera again")

if previous_tween:
previous_tween.stop()

previous_tween = create_tween()
previous_tween.tween_property(_camera, "offset", Vector2(0.0, 0.0), time_remaining)

print("duration: ", duration)
print("time_remaining: ", time_remaining)
print("duration * frequency: ", duration * frequency)
print("tweens_range: ", tweens_range)

for _n in tweens_range:
previous_tween.tween_property(_camera, "offset", Vector2(_get_shake_value(), _get_shake_value()), time_remaining)

previous_tween.tween_property(_camera, "offset", Vector2(0.0, 0.0), time_remaining)
previous_tween.play()

previous_tween.step_finished.connect(func (_x):
previous_tween = null
)


## Applies the shake using 3D context
func _apply_shake_3d() -> void:
var _camera = camera as Camera3D

assert(_camera != null, "Camera (3D) should not be null at this point, check your code before shaking the camera again")

if previous_tween != null:
previous_tween.stop()

previous_tween = create_tween()
previous_tween.tween_property(_camera, "h_offset", 0.0, time_remaining)
previous_tween.tween_property(_camera, "v_offset", 0.0, time_remaining)

print("tweens_range: ", tweens_range)
print("time_remaining: ", time_remaining)
print("duration: ", duration)
print("frequency: ", frequency)

for _n in tweens_range:
previous_tween.tween_property(_camera, "h_offset", _get_shake_value(), time_remaining)
previous_tween.tween_property(_camera, "v_offset", _get_shake_value(), time_remaining)

previous_tween.tween_property(_camera, "h_offset", 0.0, time_remaining)
previous_tween.tween_property(_camera, "v_offset", 0.0, time_remaining)
previous_tween.play()


func _get_shake_value(rerolls_remaining: int = 5) -> float:
var shake_value = randf_range(-1.0, 1.0) * strength

# Avoiding stack overflows
if max_strength == 0.0 and shake_value == 0.0:
return 0.0

# Rerolls
if shake_value == 0.0 and rerolls_remaining > 0:
return _get_shake_value(rerolls_remaining - 1)

return shake_value


func _ready() -> void:
if camera_path.is_empty():
if owner is Camera2D or owner is Camera3D:
camera = owner
else:
camera = get_node(camera_path)

assert(camera != null, "Camera is null. Set camera_path correctly.")

## Try to guess if the camera is a Camera2D
if camera is Camera2D:
camera_context = CameraContext.CAMERA_2D
## Try to guess if the camera is a Camera3D
elif camera is Camera3D:
camera_context = CameraContext.CAMERA_3D


## Resets shake 2D
func _reset_from_shake_2d() -> void:
camera.offset = Vector2.ZERO


## Resets shake 2D
func _reset_from_shake_3d() -> void:
camera.h_offset = 0.0
camera.v_offset = 0.0


## Shakes current camera by the given strength, duration, and frequency.
## [br][code]strength[/code] defaults to [code]1.0[/code]
## [br][code]duration[/code] defaults to [code]1.0[/code]
## [br][code]frequency[/code] defaults to [code]5[/code]
func shake(strength: float = 1.0, duration: float = 1.0, frequency: int = 5) -> void:
self.strength = strength
self.duration = duration
self.frequency = frequency

_apply_shake()


## Resets from shake
func reset_from_shake() -> void:
if camera is Camera2D:
_reset_from_shake_2d()
elif camera is Camera3D:
_reset_from_shake_3d()
10 changes: 10 additions & 0 deletions addons/godot_gameplay_systems/camera_shake/plugin.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@tool
extends EditorPlugin


func _enter_tree() -> void:
add_custom_type("CameraShake", "Node", load("res://addons/godot_gameplay_systems/camera_shake/nodes/camera_shake.gd"), null)


func _exit_tree() -> void:
remove_custom_type("CameraShake")
5 changes: 5 additions & 0 deletions addons/godot_gameplay_systems/plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,38 @@
extends EditorPlugin

const attributes_and_abilities_plugin_script = preload("res://addons/godot_gameplay_systems/attributes_and_abilities/plugin.gd")
const camera_shake_plugin_script = preload("res://addons/godot_gameplay_systems/camera_shake/plugin.gd")
const extended_character_nodes_script = preload("res://addons/godot_gameplay_systems/extended_character_nodes/plugin.gd")
const inventory_system_script = preload("res://addons/godot_gameplay_systems/inventory_system/plugin.gd")
const interactables_script = preload("res://addons/godot_gameplay_systems/interactables/plugin.gd")


var attributes_and_abilities_plugin: EditorPlugin
var camera_shake_plugin: EditorPlugin
var extended_character_nodes: EditorPlugin
var inventory_system: EditorPlugin
var interactables: EditorPlugin


func _init() -> void:
attributes_and_abilities_plugin = attributes_and_abilities_plugin_script.new()
camera_shake_plugin = camera_shake_plugin_script.new()
extended_character_nodes = extended_character_nodes_script.new()
inventory_system = inventory_system_script.new()
interactables = interactables_script.new()


func _enter_tree():
attributes_and_abilities_plugin._enter_tree()
camera_shake_plugin._enter_tree()
extended_character_nodes._enter_tree()
inventory_system._enter_tree()
interactables._enter_tree()


func _exit_tree():
attributes_and_abilities_plugin._exit_tree()
camera_shake_plugin._exit_tree()
extended_character_nodes._exit_tree()
inventory_system._exit_tree()
interactables._exit_tree()
21 changes: 21 additions & 0 deletions docs/camera-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Camera Shake
============

Well, you all know what a camera shake is and why is it important (at least, for a majority of games).

To implement this feature, add a `CameraShake` node into your character and point to your current character's camera.

In the code, you can trigger the shake with

```gdscript
var strength = 1.0 # the maximum shake strenght. The higher, the messier
var shake_time = 1.0 # how much it will last
var shake_frequency = 250 # will apply 250 shakes per `shake_time`
$CameraShake.shake(strength, shake_time, shake_frequency)
```

It works on both `Camera2D` and `Camera3D` nodes.

You can browse the examples in [simple_2d](https://github.com/OctoD/godot-gameplay-attributes/blob/b376e4bed656bea643ac04e2d9e0f4098febfed4/examples/simple_2d_platformer/player/player.gd) or [doom_like](https://github.com/OctoD/godot-gameplay-attributes/blob/a51cdb77d8f6f58f7239f3cf952dc170a367e136/examples/doom_like_fps/player/Player.gd)

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ icon = ExtResource("2_iai6e")
bullet_damage = 120.0
bullet_speed = 20.0
reload_time = 1.0
recoil = 0.1
name = &"shotgun"
can_stack = false
decrease_stack_on_use = false
Expand All @@ -37,7 +38,7 @@ tags_removed_on_unequip = Array[String](["shotgun.equipped"])
[sub_resource type="SphereShape3D" id="SphereShape3D_u4bui"]

[node name="FpsPickableShotgun" type="Area3D"]
transform = Transform3D(1.00813, 0, -0.0328844, 0, 1, 0, 0.0328772, 0, 1.00814, 0, 0, 0)
transform = Transform3D(-0.872187, 0, -0.506675, 0, 1, 0, 0.506675, 0, -0.872201, 0, 0, 0)
collision_layer = 8
collision_mask = 2
script = ExtResource("1_7ihpl")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ func _init() -> void:
bullet_speed = 20
## one shot per second
reload_time = 1.0
recoil = 0.25
super._init()
tags.append("right.handed")

1 change: 1 addition & 0 deletions examples/doom_like_fps/items/weapons/weapon.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class_name DoomLikeFPSWeaponItem extends Item
@export var bullet_damage: float = 10.0
@export var bullet_speed: float = 10.0
@export var reload_time: float = 0.0
@export var recoil: float = 0.05


const bullet_scene = preload("res://examples/doom_like_fps/items/weapons/bullet.tscn")
Expand Down
9 changes: 7 additions & 2 deletions examples/doom_like_fps/player/Player.gd
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var active_weapon_0: EquippedItem3D = $CameraNeck/Camera3D/LeftHand
@onready var active_weapon_1: EquippedItem3D = $CameraNeck/Camera3D/RightHand
@onready var camera_neck: Node3D = $CameraNeck
@onready var camera_shake: CameraShake = $CameraShake
@onready var equipment: Equipment = $Equipment
@onready var gameplay_attribute_map: GameplayAttributeMap = $GameplayAttributeMap
@onready var hud = $Hud
Expand Down Expand Up @@ -44,9 +45,13 @@ func _ready() -> void:
equipped_weapon = weapon
hud.handle_equipped(weapon)
)


equipment.item_activated.connect(func (weapon, _slot):
camera_shake.shake(weapon.recoil, 0.2, 10)
)

gameplay_attribute_map.attribute_changed.connect(hud.handle_attribute_changed)

hud.handle_attribute_changed(gameplay_attribute_map.get_attribute_by_name("health"))
hud.handle_attribute_changed(gameplay_attribute_map.get_attribute_by_name("armor"))

Expand Down
9 changes: 8 additions & 1 deletion examples/doom_like_fps/player/Player.tscn
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
[gd_scene load_steps=11 format=3 uid="uid://bjwuhfb2wcj1x"]
[gd_scene load_steps=12 format=3 uid="uid://bjwuhfb2wcj1x"]

[ext_resource type="Script" path="res://examples/doom_like_fps/player/Player.gd" id="1_diqix"]
[ext_resource type="Script" path="res://addons/godot_gameplay_systems/inventory_system/nodes/equipped_item_3d.gd" id="2_jwtl3"]
[ext_resource type="Script" path="res://addons/godot_gameplay_systems/attributes_and_abilities/nodes/gameplay_attribute_map.gd" id="2_pwjr5"]
[ext_resource type="Script" path="res://addons/godot_gameplay_systems/attributes_and_abilities/resources/attribute.gd" id="3_gseup"]
[ext_resource type="Resource" uid="uid://2s6702ugdpni" path="res://examples/doom_like_fps/doom_like_pg_fps_attributes.tres" id="4_fxibq"]
[ext_resource type="Script" path="res://addons/godot_gameplay_systems/inventory_system/nodes/equipment.gd" id="5_pje3c"]
[ext_resource type="Script" path="res://addons/godot_gameplay_systems/camera_shake/nodes/camera_shake.gd" id="8_ofxhn"]
[ext_resource type="Script" path="res://examples/doom_like_fps/player/hud.gd" id="10_wtnkq"]

[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_3660s"]
Expand Down Expand Up @@ -97,3 +98,9 @@ layout_mode = 2

[node name="WeaponIcon" type="TextureRect" parent="Hud/HBoxContainer/HBoxContainer/AspectRatioContainer"]
layout_mode = 2

[node name="CameraShake" type="Node" parent="."]
script = ExtResource("8_ofxhn")
camera_path = NodePath("../CameraNeck/Camera3D")
max_strength = 1.0
max_frequency = 10
6 changes: 6 additions & 0 deletions examples/simple_2d_platformer/player/player.gd
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var is_dead: bool:

@onready var ability_container: AbilityContainer = $AbilityContainer
@onready var animated_sprite: AnimatedSprite2D = $CollisionShape2d/AnimatedSprite2D
@onready var camera_shake: CameraShake = $CameraShake
@onready var gameplay_attribute_map: GameplayAttributeMap = $GameplayAttributeMap
@onready var hud = $Camera2D/CanvasLayer/Hud
@onready var inventory: Inventory = $Inventory
Expand All @@ -36,6 +37,8 @@ func _handle_attribute_changed(spec: AttributeSpec) -> void:
if spec.current_value <= 0.0:
print("Added dead tag to player ability container")
ability_container.add_tag("dead")
## Shake for the death
camera_shake.shake(50.0, 1, 250)


func _ready() -> void:
Expand All @@ -62,6 +65,9 @@ func _input(event: InputEvent) -> void:
if fireball:
ability_container.activate_one(fireball)

if not is_dead:
camera_shake.shake(3.0, 0.5, 400)


func _physics_process(delta: float) -> void:
# Add the gravity.
Expand Down
Loading

0 comments on commit 5aeb5ae

Please sign in to comment.