initial
This commit is contained in:
59
cutscene/Action.gd
Normal file
59
cutscene/Action.gd
Normal file
@@ -0,0 +1,59 @@
|
||||
class_name Action
|
||||
extends RefCounted
|
||||
|
||||
# Action states
|
||||
enum State {
|
||||
PENDING, # Action is waiting to be executed
|
||||
RUNNING, # Action is currently executing
|
||||
COMPLETED, # Action has finished executing
|
||||
FAILED # Action failed during execution
|
||||
}
|
||||
|
||||
# Public properties
|
||||
var state: int = State.PENDING
|
||||
var name: String = "Action"
|
||||
|
||||
# Signals
|
||||
signal started()
|
||||
signal completed()
|
||||
signal failed(error_message)
|
||||
signal stopped()
|
||||
|
||||
# Virtual methods to be implemented by subclasses
|
||||
func start() -> void:
|
||||
# Start executing the action
|
||||
# This method should be overridden by subclasses
|
||||
pass
|
||||
|
||||
func update(delta: float) -> void:
|
||||
# Update the action (called every frame while running)
|
||||
# This method can be overridden by subclasses
|
||||
pass
|
||||
|
||||
func is_completed() -> bool:
|
||||
# Check if the action has completed
|
||||
# This method should be overridden by subclasses
|
||||
return state == State.COMPLETED
|
||||
|
||||
func stop() -> void:
|
||||
# Stop the action (if it's running)
|
||||
# This method can be overridden by subclasses
|
||||
if state == State.RUNNING:
|
||||
state = State.FAILED
|
||||
stopped.emit()
|
||||
|
||||
# Helper methods
|
||||
func _set_running() -> void:
|
||||
# Set the action state to RUNNING and emit started signal
|
||||
state = State.RUNNING
|
||||
started.emit()
|
||||
|
||||
func _set_completed() -> void:
|
||||
# Set the action state to COMPLETED and emit completed signal
|
||||
state = State.COMPLETED
|
||||
completed.emit()
|
||||
|
||||
func _set_failed(error_message: String) -> void:
|
||||
# Set the action state to FAILED and emit failed signal
|
||||
state = State.FAILED
|
||||
failed.emit(error_message)
|
||||
228
cutscene/CutsceneManager.gd
Normal file
228
cutscene/CutsceneManager.gd
Normal file
@@ -0,0 +1,228 @@
|
||||
class_name CutsceneManager
|
||||
extends Node
|
||||
|
||||
# Action queues
|
||||
var sequential_actions: Array = [] # Actions that run one after another
|
||||
var current_action: Action = null # The action currently being executed
|
||||
var action_index: int = 0 # Index of the next sequential action
|
||||
|
||||
# Parallel actions
|
||||
var parallel_groups: Array = [] # Groups of actions running simultaneously
|
||||
var active_parallel_groups: Array = [] # Currently running parallel action groups
|
||||
|
||||
# State management
|
||||
enum State {
|
||||
IDLE, # Not running any cutscene
|
||||
RUNNING, # Currently executing a cutscene
|
||||
PAUSED # Cutscene is paused
|
||||
}
|
||||
|
||||
var state: int = State.IDLE
|
||||
var is_active: bool = false
|
||||
|
||||
# Signals
|
||||
signal cutscene_started()
|
||||
signal cutscene_completed()
|
||||
signal cutscene_paused()
|
||||
signal cutscene_resumed()
|
||||
signal action_started(action)
|
||||
signal action_completed(action)
|
||||
signal action_failed(action, error_message)
|
||||
|
||||
# Public methods
|
||||
func start() -> void:
|
||||
# Start executing the cutscene
|
||||
if state != State.IDLE:
|
||||
return
|
||||
|
||||
state = State.RUNNING
|
||||
is_active = true
|
||||
emit_signal("cutscene_started")
|
||||
|
||||
# Start first action
|
||||
_execute_next_action()
|
||||
|
||||
func pause() -> void:
|
||||
# Pause the current cutscene
|
||||
if state == State.RUNNING:
|
||||
state = State.PAUSED
|
||||
emit_signal("cutscene_paused")
|
||||
|
||||
func resume() -> void:
|
||||
# Resume a paused cutscene
|
||||
if state == State.PAUSED:
|
||||
state = State.RUNNING
|
||||
emit_signal("cutscene_resumed")
|
||||
|
||||
func stop() -> void:
|
||||
# Stop the current cutscene and reset
|
||||
if current_action != null:
|
||||
current_action.stop()
|
||||
current_action = null
|
||||
|
||||
# Stop all active parallel groups
|
||||
for group in active_parallel_groups:
|
||||
for action in group["actions"]:
|
||||
if action.state == action.State.RUNNING:
|
||||
action.stop()
|
||||
|
||||
_reset()
|
||||
|
||||
func add_action(action: Action) -> void:
|
||||
# Add a single action to the sequential queue
|
||||
if state != State.IDLE:
|
||||
print("Warning: Cannot add actions while cutscene is running")
|
||||
return
|
||||
|
||||
sequential_actions.append(action)
|
||||
|
||||
func add_parallel_actions(actions: Array) -> void:
|
||||
# Add a group of actions to run in parallel
|
||||
if state != State.IDLE:
|
||||
print("Warning: Cannot add actions while cutscene is running")
|
||||
return
|
||||
|
||||
if actions.size() == 0:
|
||||
return
|
||||
|
||||
# Create a parallel action group
|
||||
var group = {
|
||||
"actions": actions,
|
||||
"completed_count": 0,
|
||||
"total_count": actions.size()
|
||||
}
|
||||
|
||||
# Add to sequential actions as a single item
|
||||
sequential_actions.append(group)
|
||||
|
||||
# Internal methods
|
||||
func _process(delta: float) -> void:
|
||||
# Main update loop
|
||||
if state != State.RUNNING:
|
||||
return
|
||||
|
||||
# Update current sequential action
|
||||
if current_action != null and current_action.state == current_action.State.RUNNING:
|
||||
current_action.update(delta)
|
||||
|
||||
# Update active parallel groups
|
||||
for group in active_parallel_groups:
|
||||
for action in group["actions"]:
|
||||
if action.state == action.State.RUNNING:
|
||||
action.update(delta)
|
||||
|
||||
func _execute_next_action() -> void:
|
||||
# Start executing the next sequential action
|
||||
if action_index >= sequential_actions.size():
|
||||
# No more actions, cutscene complete
|
||||
_on_cutscene_completed()
|
||||
return
|
||||
|
||||
var action_item = sequential_actions[action_index]
|
||||
action_index += 1
|
||||
|
||||
# Check if this is a parallel group or single action
|
||||
if typeof(action_item) == TYPE_DICTIONARY and action_item.has("actions"):
|
||||
# This is a parallel group
|
||||
_execute_parallel_group(action_item)
|
||||
else:
|
||||
# This is a single action
|
||||
_execute_single_action(action_item)
|
||||
|
||||
func _execute_single_action(action: Action) -> void:
|
||||
# Execute a single action
|
||||
current_action = action
|
||||
|
||||
# Connect to action signals
|
||||
if not action.is_connected("completed", _on_action_completed):
|
||||
action.connect("completed", _on_action_completed.bind(action))
|
||||
if not action.is_connected("failed", _on_action_failed):
|
||||
action.connect("failed", _on_action_failed.bind(action))
|
||||
if not action.is_connected("started", _on_action_started):
|
||||
action.connect("started", _on_action_started.bind(action))
|
||||
|
||||
# Start the action
|
||||
emit_signal("action_started", action)
|
||||
action.start()
|
||||
|
||||
func _execute_parallel_group(group: Dictionary) -> void:
|
||||
# Execute a group of parallel actions
|
||||
var actions = group["actions"]
|
||||
|
||||
# Reset completion count
|
||||
group["completed_count"] = 0
|
||||
|
||||
# Add to active parallel groups
|
||||
active_parallel_groups.append(group)
|
||||
|
||||
# Connect to each action's signals
|
||||
for action in actions:
|
||||
if not action.is_connected("completed", _on_parallel_action_completed):
|
||||
action.connect("completed", _on_parallel_action_completed.bind(group, action))
|
||||
if not action.is_connected("failed", _on_parallel_action_failed):
|
||||
action.connect("failed", _on_parallel_action_failed.bind(group, action))
|
||||
if not action.is_connected("started", _on_action_started):
|
||||
action.connect("started", _on_action_started.bind(action))
|
||||
|
||||
# Emit started signal and start action
|
||||
emit_signal("action_started", action)
|
||||
action.start()
|
||||
|
||||
func _on_action_started(action: Action) -> void:
|
||||
# Handle action started
|
||||
emit_signal("action_started", action)
|
||||
|
||||
func _on_action_completed(action: Action) -> void:
|
||||
# Handle action completion
|
||||
if action == current_action:
|
||||
current_action = null
|
||||
emit_signal("action_completed", action)
|
||||
# Move to next action
|
||||
_execute_next_action()
|
||||
|
||||
func _on_action_failed(action: Action, error_message: String) -> void:
|
||||
# Handle action failure
|
||||
if action == current_action:
|
||||
current_action = null
|
||||
|
||||
emit_signal("action_failed", action, error_message)
|
||||
print("Action failed: %s - %s" % [action.name, error_message])
|
||||
# Stop the cutscene
|
||||
stop()
|
||||
|
||||
func _on_parallel_action_completed(group: Dictionary, action: Action) -> void:
|
||||
# Increment completed count
|
||||
group["completed_count"] += 1
|
||||
|
||||
emit_signal("action_completed", action)
|
||||
|
||||
# Check if all actions in group are completed
|
||||
if group["completed_count"] >= group["total_count"]:
|
||||
# Remove from active groups
|
||||
active_parallel_groups.erase(group)
|
||||
|
||||
# Continue with next sequential action
|
||||
_execute_next_action()
|
||||
|
||||
func _on_parallel_action_failed(group: Dictionary, action: Action, error_message: String) -> void:
|
||||
# Handle parallel action failure
|
||||
emit_signal("action_failed", action, error_message)
|
||||
print("Parallel action failed: %s - %s" % [action.name, error_message])
|
||||
# Stop the cutscene
|
||||
stop()
|
||||
|
||||
func _on_cutscene_completed() -> void:
|
||||
# Handle cutscene completion
|
||||
state = State.IDLE
|
||||
is_active = false
|
||||
emit_signal("cutscene_completed")
|
||||
|
||||
func _reset() -> void:
|
||||
# Reset the manager to initial state
|
||||
sequential_actions.clear()
|
||||
parallel_groups.clear()
|
||||
active_parallel_groups.clear()
|
||||
current_action = null
|
||||
action_index = 0
|
||||
state = State.IDLE
|
||||
is_active = false
|
||||
135
cutscene/README.md
Normal file
135
cutscene/README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Cutscene System for Godot 4.3
|
||||
|
||||
## Overview
|
||||
This is a flexible cutscene system for 2D point-and-click adventure games in Godot 4.3. It supports both sequential and parallel action execution, with a focus on extensibility and dynamic behavior.
|
||||
|
||||
## Features
|
||||
- Sequential action execution
|
||||
- Parallel action execution
|
||||
- Extensible action system
|
||||
- Signal-based completion system
|
||||
- Error handling and recovery
|
||||
|
||||
## System Components
|
||||
|
||||
### Core Classes
|
||||
- `Action`: Base class for all actions
|
||||
- `CutsceneManager`: Orchestrates action execution
|
||||
|
||||
### Action Types
|
||||
- `MoveAction`: Move characters to specific positions
|
||||
- `TurnAction`: Rotate characters to face targets
|
||||
- `DialogueAction`: Display dialogue text
|
||||
- `AnimationAction`: Play character animations
|
||||
- `WaitAction`: Time-based delays
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Setup
|
||||
```gdscript
|
||||
# Create a cutscene manager
|
||||
var cutscene_manager = CutsceneManager.new()
|
||||
add_child(cutscene_manager)
|
||||
|
||||
# Add sequential actions
|
||||
cutscene_manager.add_action(MoveAction.new(character, Vector2(100, 100)))
|
||||
cutscene_manager.add_action(DialogueAction.new(character, "Hello, world!"))
|
||||
|
||||
# Start the cutscene
|
||||
cutscene_manager.start()
|
||||
```
|
||||
|
||||
### Parallel Actions
|
||||
```gdscript
|
||||
# Create parallel actions
|
||||
var parallel_actions = [
|
||||
MoveAction.new(character1, Vector2(200, 200)),
|
||||
MoveAction.new(character2, Vector2(300, 300))
|
||||
]
|
||||
cutscene_manager.add_parallel_actions(parallel_actions)
|
||||
```
|
||||
|
||||
### The Example Scenario
|
||||
The system supports complex sequences like the one described in the requirements:
|
||||
|
||||
```gdscript
|
||||
# 1. Character1 moves to 234, 591
|
||||
# 2. At the same time Character2 moves to 912, 235
|
||||
# 3. When both arrive, character2 turns to character1
|
||||
# 4. Character2 says a dialogue line to character1
|
||||
# 5. A special animation (shocked) is played for character1
|
||||
|
||||
var cutscene = CutsceneManager.new()
|
||||
add_child(cutscene)
|
||||
|
||||
# Add parallel movements
|
||||
var moves = [
|
||||
MoveAction.new(character1, Vector2(234, 591)),
|
||||
MoveAction.new(character2, Vector2(912, 235))
|
||||
]
|
||||
cutscene.add_parallel_actions(moves)
|
||||
|
||||
// Add sequential actions
|
||||
cutscene.add_action(TurnAction.new(character2, character1))
|
||||
cutscene.add_action(DialogueAction.new(character2, "Hello there!"))
|
||||
cutscene.add_action(AnimationAction.new(character1, "shocked"))
|
||||
|
||||
cutscene.start()
|
||||
```
|
||||
|
||||
## Extending the System
|
||||
|
||||
### Creating New Action Types
|
||||
1. Create a new class that extends `Action`
|
||||
2. Implement the required methods:
|
||||
- `start()`: Initialize and begin the action
|
||||
- `update(delta)`: Update the action each frame (optional)
|
||||
- `is_completed()`: Return true when the action is finished
|
||||
3. Use the action in cutscenes like any other
|
||||
|
||||
```gdscript
|
||||
class_name CustomAction
|
||||
extends "res://cutscene/Action.gd"
|
||||
|
||||
func _init(custom_parameter):
|
||||
# Initialize custom properties
|
||||
pass
|
||||
|
||||
func start() -> void:
|
||||
# Start the action
|
||||
._set_running()
|
||||
|
||||
func update(delta: float) -> void:
|
||||
# Update the action each frame
|
||||
pass
|
||||
|
||||
func is_completed() -> bool:
|
||||
# Return true when the action is completed
|
||||
return state == State.COMPLETED
|
||||
```
|
||||
|
||||
## File Structure
|
||||
```
|
||||
/cutscene/
|
||||
├── Action.gd
|
||||
├── CutsceneManager.gd
|
||||
├── actions/
|
||||
│ ├── MoveAction.gd
|
||||
│ ├── TurnAction.gd
|
||||
│ ├── DialogueAction.gd
|
||||
│ ├── AnimationAction.gd
|
||||
│ └── WaitAction.gd
|
||||
├── examples/
|
||||
│ ├── sample_cutscene.tscn
|
||||
│ ├── sample_cutscene.gd
|
||||
│ ├── animated_character.tscn
|
||||
│ └── animated_character.gd
|
||||
└── tests/
|
||||
└── test_cutscene_system.gd
|
||||
```
|
||||
|
||||
## Testing
|
||||
Run the test script at `cutscene/tests/test_cutscene_system.gd` to verify the system is working correctly.
|
||||
|
||||
## License
|
||||
This cutscene system is provided as-is for educational and prototyping purposes.
|
||||
55
cutscene/actions/AnimationAction.gd
Normal file
55
cutscene/actions/AnimationAction.gd
Normal file
@@ -0,0 +1,55 @@
|
||||
class_name AnimationAction
|
||||
extends "res://cutscene/Action.gd"
|
||||
|
||||
# Properties
|
||||
var character: Node2D # The character to animate
|
||||
var animation_name: String # Name of the animation to play
|
||||
var loop: bool = false # Whether to loop the animation
|
||||
|
||||
func _init(character_node: Node2D, anim_name: String, should_loop: bool = false) -> void:
|
||||
character = character_node
|
||||
animation_name = anim_name
|
||||
loop = should_loop
|
||||
name = "AnimationAction"
|
||||
|
||||
func start() -> void:
|
||||
if character == null:
|
||||
self._set_failed("Character is null")
|
||||
return
|
||||
|
||||
# Check if character has an AnimationPlayer
|
||||
var anim_player = _get_animation_player()
|
||||
if anim_player == null:
|
||||
self._set_failed("Character has no AnimationPlayer")
|
||||
return
|
||||
|
||||
# Connect to animation finished signal for non-looping animations
|
||||
if not loop:
|
||||
if not anim_player.is_connected("animation_finished", _on_animation_finished):
|
||||
anim_player.connect("animation_finished", _on_animation_finished.bind(anim_player))
|
||||
|
||||
# Play the animation
|
||||
anim_player.play(animation_name)
|
||||
self._set_running()
|
||||
|
||||
# For looping animations, we complete immediately
|
||||
# (they would be stopped by another action)
|
||||
if loop:
|
||||
self._set_completed()
|
||||
|
||||
func _get_animation_player() -> AnimationPlayer:
|
||||
# Try to find AnimationPlayer as a child
|
||||
for child in character.get_children():
|
||||
if child is AnimationPlayer:
|
||||
return child
|
||||
|
||||
# Try to find AnimationPlayer in the scene tree
|
||||
return character.get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
|
||||
func _on_animation_finished(anim_name: String, anim_player: AnimationPlayer) -> void:
|
||||
# Make sure this is for our animation
|
||||
if anim_name == animation_name and not loop:
|
||||
self._set_completed()
|
||||
|
||||
func is_completed() -> bool:
|
||||
return state == State.COMPLETED
|
||||
75
cutscene/actions/DialogueAction.gd
Normal file
75
cutscene/actions/DialogueAction.gd
Normal file
@@ -0,0 +1,75 @@
|
||||
class_name DialogueAction
|
||||
extends "res://cutscene/Action.gd"
|
||||
|
||||
# Properties
|
||||
var character: Node2D # The speaking character (optional)
|
||||
var text: String # Dialogue text
|
||||
var duration: float = 0.0 # Duration to display (0 = manual advance)
|
||||
|
||||
func _init(speaking_character: Node2D, dialogue_text: String, display_duration: float = 0.0) -> void:
|
||||
character = speaking_character
|
||||
text = dialogue_text
|
||||
duration = display_duration
|
||||
name = "DialogueAction"
|
||||
|
||||
func start() -> void:
|
||||
# Display the dialogue text
|
||||
_display_dialogue()
|
||||
self._set_running()
|
||||
|
||||
# If duration is 0, this is a manual advance action
|
||||
if duration <= 0:
|
||||
# For demo purposes, we'll complete immediately
|
||||
# In a real game, this would wait for player input
|
||||
self._set_completed()
|
||||
else:
|
||||
# Start a timer for automatic completion
|
||||
var timer = Timer.new()
|
||||
timer.wait_time = duration
|
||||
timer.one_shot = true
|
||||
timer.connect("timeout", _on_timer_timeout)
|
||||
|
||||
# Add timer as a child to ensure it processes
|
||||
if Engine.get_main_loop().current_scene:
|
||||
Engine.get_main_loop().current_scene.add_child(timer)
|
||||
#else:
|
||||
## Fallback if we can't access the main scene
|
||||
#var root = get_tree().root if get_tree() else null
|
||||
#if root and root.get_child_count() > 0:
|
||||
#root.get_child(0).add_child(timer)
|
||||
#else:
|
||||
## Last resort - connect directly and manage manually
|
||||
#timer.connect("timeout", _on_timer_timeout_direct)
|
||||
#timer.start(duration)
|
||||
#return
|
||||
|
||||
timer.connect("timeout", _cleanup_timer.bind(timer))
|
||||
timer.start()
|
||||
|
||||
func _display_dialogue() -> void:
|
||||
# In a real implementation, this would interface with a dialogue system
|
||||
# For now, we'll simulate by printing to console
|
||||
var character_name = character.name if character and character.name else "Unknown"
|
||||
print("%s: %s" % [character_name, text])
|
||||
|
||||
func _on_timer_timeout() -> void:
|
||||
self._set_completed()
|
||||
|
||||
func _on_timer_timeout_direct() -> void:
|
||||
# Direct timeout handler for when we can't add the timer to the scene tree
|
||||
self._set_completed()
|
||||
|
||||
func _cleanup_timer(timer: Timer) -> void:
|
||||
# Clean up the timer
|
||||
if timer and timer.get_parent():
|
||||
timer.get_parent().remove_child(timer)
|
||||
if timer:
|
||||
timer.queue_free()
|
||||
self._set_completed()
|
||||
|
||||
func is_completed() -> bool:
|
||||
return state == State.COMPLETED
|
||||
|
||||
func stop() -> void:
|
||||
# Clean up any timers when stopping
|
||||
super.stop()
|
||||
54
cutscene/actions/MoveAction.gd
Normal file
54
cutscene/actions/MoveAction.gd
Normal file
@@ -0,0 +1,54 @@
|
||||
class_name MoveAction
|
||||
extends "res://cutscene/Action.gd"
|
||||
|
||||
# Properties
|
||||
var character: Node2D # The character to move
|
||||
var target_position: Vector2 # Target position
|
||||
var speed: float = 100.0 # Movement speed (pixels/second)
|
||||
var threshold: float = 5.0 # Distance threshold for completion
|
||||
|
||||
# Internal
|
||||
var start_position: Vector2
|
||||
var distance: float
|
||||
var traveled: float = 0.0
|
||||
|
||||
func _init(character_node: Node2D, target: Vector2, move_speed: float = 100.0) -> void:
|
||||
character = character_node
|
||||
target_position = target
|
||||
speed = move_speed
|
||||
name = "MoveAction"
|
||||
|
||||
func start() -> void:
|
||||
if character == null:
|
||||
self._set_failed("Character is null")
|
||||
return
|
||||
|
||||
start_position = character.position
|
||||
distance = start_position.distance_to(target_position)
|
||||
traveled = 0.0
|
||||
self._set_running()
|
||||
|
||||
func update(delta: float) -> void:
|
||||
if state != State.RUNNING:
|
||||
return
|
||||
|
||||
if character == null:
|
||||
self._set_failed("Character was destroyed during action")
|
||||
return
|
||||
|
||||
# Calculate movement for this frame
|
||||
var frame_distance = speed * delta
|
||||
traveled += frame_distance
|
||||
|
||||
# If we've reached or overshot the target
|
||||
if traveled >= distance:
|
||||
character.position = target_position
|
||||
self._set_completed()
|
||||
return
|
||||
|
||||
# Move character along the path
|
||||
var direction = (target_position - start_position).normalized()
|
||||
character.position = start_position + direction * traveled
|
||||
|
||||
func is_completed() -> bool:
|
||||
return state == State.COMPLETED
|
||||
69
cutscene/actions/TurnAction.gd
Normal file
69
cutscene/actions/TurnAction.gd
Normal file
@@ -0,0 +1,69 @@
|
||||
class_name TurnAction
|
||||
extends "res://cutscene/Action.gd"
|
||||
|
||||
# Properties
|
||||
var character: Node2D # The character to turn
|
||||
var target: Variant # Either a Vector2 position or another Node2D
|
||||
var turn_speed: float = 2.0 # Rotation speed (radians/second)
|
||||
|
||||
func _init(character_node: Node2D, turn_target: Variant, speed: float = 2.0) -> void:
|
||||
character = character_node
|
||||
target = turn_target
|
||||
turn_speed = speed
|
||||
name = "TurnAction"
|
||||
|
||||
func start() -> void:
|
||||
if character == null:
|
||||
self._set_failed("Character is null")
|
||||
return
|
||||
self._set_running()
|
||||
|
||||
func update(delta: float) -> void:
|
||||
if state != State.RUNNING:
|
||||
return
|
||||
|
||||
if character == null:
|
||||
self._set_failed("Character was destroyed during action")
|
||||
return
|
||||
|
||||
# Calculate target rotation
|
||||
var target_position: Vector2
|
||||
if target is Vector2:
|
||||
target_position = target
|
||||
elif target is Node2D:
|
||||
target_position = target.position
|
||||
else:
|
||||
self._set_failed("Invalid target type")
|
||||
return
|
||||
|
||||
var direction = target_position - character.position
|
||||
var target_rotation = atan2(direction.y, direction.x)
|
||||
|
||||
# Rotate toward target
|
||||
var angle_diff = _angle_difference(character.rotation, target_rotation)
|
||||
|
||||
# If we're close enough, complete the action
|
||||
if abs(angle_diff) < 0.01:
|
||||
character.rotation = target_rotation
|
||||
self._set_completed()
|
||||
return
|
||||
|
||||
# Rotate based on turn speed
|
||||
var rotation_amount = sign(angle_diff) * turn_speed * delta
|
||||
if abs(rotation_amount) > abs(angle_diff):
|
||||
character.rotation = target_rotation
|
||||
self._set_completed()
|
||||
else:
|
||||
character.rotation += rotation_amount
|
||||
|
||||
func is_completed() -> bool:
|
||||
return state == State.COMPLETED
|
||||
|
||||
# Helper function to calculate angle difference
|
||||
func _angle_difference(from: float, to: float) -> float:
|
||||
var diff = fmod(to - from, 2.0 * PI)
|
||||
if diff < -PI:
|
||||
diff += 2.0 * PI
|
||||
elif diff > PI:
|
||||
diff -= 2.0 * PI
|
||||
return diff
|
||||
25
cutscene/actions/WaitAction.gd
Normal file
25
cutscene/actions/WaitAction.gd
Normal file
@@ -0,0 +1,25 @@
|
||||
class_name WaitAction
|
||||
extends "res://cutscene/Action.gd"
|
||||
|
||||
# Properties
|
||||
var duration: float # Time to wait in seconds
|
||||
var elapsed_time: float = 0.0
|
||||
|
||||
func _init(wait_duration: float) -> void:
|
||||
duration = wait_duration
|
||||
name = "WaitAction"
|
||||
|
||||
func start() -> void:
|
||||
elapsed_time = 0.0
|
||||
self._set_running()
|
||||
|
||||
func update(delta: float) -> void:
|
||||
if state != State.RUNNING:
|
||||
return
|
||||
|
||||
elapsed_time += delta
|
||||
if elapsed_time >= duration:
|
||||
self._set_completed()
|
||||
|
||||
func is_completed() -> bool:
|
||||
return state == State.COMPLETED
|
||||
10
cutscene/examples/animated_character.tscn
Normal file
10
cutscene/examples/animated_character.tscn
Normal file
@@ -0,0 +1,10 @@
|
||||
[gd_scene format=3 uid="uid://animatedcharacter"]
|
||||
|
||||
[ext_resource type="Script" path="res://cutscene/examples/animated_character.gd" id="1"]
|
||||
|
||||
[node name="AnimatedCharacter" type="Node2D"]
|
||||
script = ExtResource( "1" )
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
|
||||
80
cutscene/examples/sample_cutscene.gd
Normal file
80
cutscene/examples/sample_cutscene.gd
Normal file
@@ -0,0 +1,80 @@
|
||||
extends Node2D
|
||||
|
||||
# Character nodes
|
||||
@onready var character1: Node2D = $Character1
|
||||
@onready var character2: Node2D = $Character2
|
||||
|
||||
# Cutscene manager
|
||||
var cutscene_manager: CutsceneManager
|
||||
|
||||
func _ready() -> void:
|
||||
# Initialize the cutscene system
|
||||
setup_cutscene()
|
||||
|
||||
# Start the cutscene after a short delay to see the initial positions
|
||||
var start_timer = Timer.new()
|
||||
start_timer.wait_time = 1.0
|
||||
start_timer.one_shot = true
|
||||
start_timer.connect("timeout", start_cutscene)
|
||||
add_child(start_timer)
|
||||
start_timer.start()
|
||||
|
||||
func setup_cutscene() -> void:
|
||||
# Create the cutscene manager
|
||||
cutscene_manager = CutsceneManager.new()
|
||||
add_child(cutscene_manager)
|
||||
|
||||
# Connect to cutscene signals
|
||||
cutscene_manager.connect("cutscene_started", _on_cutscene_started)
|
||||
cutscene_manager.connect("cutscene_completed", _on_cutscene_completed)
|
||||
cutscene_manager.connect("action_started", _on_action_started)
|
||||
cutscene_manager.connect("action_completed", _on_action_completed)
|
||||
|
||||
# Create the action sequence as described in requirements
|
||||
|
||||
# 1. & 2. Characters move simultaneously
|
||||
var parallel_moves = [
|
||||
MoveAction.new(character1, Vector2(234, 591), 100.0), # Character1 moves
|
||||
MoveAction.new(character2, Vector2(912, 235), 100.0) # Character2 moves
|
||||
]
|
||||
cutscene_manager.add_parallel_actions(parallel_moves)
|
||||
|
||||
# 3. Character2 turns to face Character1
|
||||
var turn_action = TurnAction.new(character2, character1, 1.0)
|
||||
cutscene_manager.add_action(turn_action)
|
||||
|
||||
# 4. Character2 says dialogue
|
||||
var dialogue_action = DialogueAction.new(character2, "Hello there, friend!", 2.0)
|
||||
cutscene_manager.add_action(dialogue_action)
|
||||
|
||||
# 5. Character1 plays shocked animation (simulated with a wait since we don't have an actual animation)
|
||||
var animation_action = WaitAction.new(1.0) # Simulate animation with wait
|
||||
cutscene_manager.add_action(animation_action)
|
||||
|
||||
# Add a final dialogue from character1
|
||||
var final_dialogue = DialogueAction.new(character1, "That was surprising!", 2.0)
|
||||
cutscene_manager.add_action(final_dialogue)
|
||||
|
||||
func start_cutscene() -> void:
|
||||
print("Starting cutscene...")
|
||||
cutscene_manager.start()
|
||||
|
||||
func _on_cutscene_started() -> void:
|
||||
print("Cutscene started!")
|
||||
|
||||
func _on_cutscene_completed() -> void:
|
||||
print("Cutscene completed!")
|
||||
print("Final positions:")
|
||||
print("Character1: %s" % character1.position)
|
||||
print("Character2: %s" % character2.position)
|
||||
|
||||
func _on_action_started(action: Action) -> void:
|
||||
print("Action started: %s" % action.name)
|
||||
|
||||
func _on_action_completed(action: Action) -> void:
|
||||
print("Action completed: %s" % action.name)
|
||||
|
||||
# Clean up when the node is removed
|
||||
func _exit_tree() -> void:
|
||||
if cutscene_manager:
|
||||
cutscene_manager.queue_free()
|
||||
18
cutscene/examples/sample_cutscene.tscn
Normal file
18
cutscene/examples/sample_cutscene.tscn
Normal file
@@ -0,0 +1,18 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bx826fm1kd3wk"]
|
||||
|
||||
[ext_resource type="Script" path="res://cutscene/examples/sample_cutscene.gd" id="1"]
|
||||
|
||||
[node name="SampleCutscene" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Character1" type="Node2D" parent="."]
|
||||
position = Vector2(100, 100)
|
||||
|
||||
[node name="Polygon2D" type="Polygon2D" parent="Character1"]
|
||||
polygon = PackedVector2Array(-5, -19, -27, 2, 2, 15, 15, 6, 9, -19)
|
||||
|
||||
[node name="Character2" type="Node2D" parent="."]
|
||||
position = Vector2(200, 100)
|
||||
|
||||
[node name="Polygon2D" type="Polygon2D" parent="Character2"]
|
||||
polygon = PackedVector2Array(10, -25, -43, 1, -14, 40, 48, 13)
|
||||
95
cutscene/tests/test_cutscene_system.gd
Normal file
95
cutscene/tests/test_cutscene_system.gd
Normal file
@@ -0,0 +1,95 @@
|
||||
extends Node2D
|
||||
|
||||
# Test the cutscene system
|
||||
func _ready() -> void:
|
||||
print("Testing cutscene system...")
|
||||
|
||||
# Create test characters
|
||||
var char1 = Node2D.new()
|
||||
char1.name = "TestCharacter1"
|
||||
add_child(char1)
|
||||
|
||||
var char2 = Node2D.new()
|
||||
char2.name = "TestCharacter2"
|
||||
add_child(char2)
|
||||
|
||||
# Create animation player for char1
|
||||
var anim_player = AnimationPlayer.new()
|
||||
anim_player.name = "AnimationPlayer"
|
||||
char1.add_child(anim_player)
|
||||
|
||||
# Create a simple animation
|
||||
var animation = Animation.new()
|
||||
animation.name = "test_animation"
|
||||
animation.length = 1.0
|
||||
anim_player.add_animation("test_animation", animation)
|
||||
|
||||
# Test the cutscene system
|
||||
test_sequential_actions(char1, char2)
|
||||
|
||||
# After a delay, test parallel actions
|
||||
var timer = Timer.new()
|
||||
timer.wait_time = 3.0
|
||||
timer.one_shot = true
|
||||
timer.connect("timeout", test_parallel_actions.bind(char1, char2))
|
||||
add_child(timer)
|
||||
timer.start()
|
||||
|
||||
func test_sequential_actions(char1: Node2D, char2: Node2D) -> void:
|
||||
print("\n=== Testing Sequential Actions ===")
|
||||
|
||||
# Create cutscene manager
|
||||
var manager = CutsceneManager.new()
|
||||
add_child(manager)
|
||||
|
||||
# Connect signals
|
||||
manager.connect("cutscene_completed", _on_test_completed.bind("sequential"))
|
||||
|
||||
# Add sequential actions
|
||||
manager.add_action(MoveAction.new(char1, Vector2(100, 100), 50.0))
|
||||
manager.add_action(WaitAction.new(0.5))
|
||||
manager.add_action(TurnAction.new(char1, Vector2(200, 200), 1.0))
|
||||
manager.add_action(DialogueAction.new(char1, "Hello, world!", 1.0))
|
||||
manager.add_action(AnimationAction.new(char1, "test_animation", false))
|
||||
|
||||
# Start the cutscene
|
||||
manager.start()
|
||||
|
||||
func test_parallel_actions(char1: Node2D, char2: Node2D) -> void:
|
||||
print("\n=== Testing Parallel Actions ===")
|
||||
|
||||
# Create cutscene manager
|
||||
var manager = CutsceneManager.new()
|
||||
add_child(manager)
|
||||
|
||||
# Connect signals
|
||||
manager.connect("cutscene_completed", _on_test_completed.bind("parallel"))
|
||||
|
||||
# Add parallel actions
|
||||
var parallel_group = [
|
||||
MoveAction.new(char1, Vector2(300, 300), 100.0),
|
||||
MoveAction.new(char2, Vector2(400, 400), 100.0)
|
||||
]
|
||||
manager.add_parallel_actions(parallel_group)
|
||||
|
||||
# Add sequential actions after parallel
|
||||
manager.add_action(TurnAction.new(char1, char2, 2.0))
|
||||
manager.add_action(DialogueAction.new(char1, "We moved together!", 1.5))
|
||||
|
||||
# Start the cutscene
|
||||
manager.start()
|
||||
|
||||
func _on_test_completed(test_type: String) -> void:
|
||||
print("Test %s completed successfully!" % test_type)
|
||||
|
||||
# Clean up after a delay
|
||||
var cleanup_timer = Timer.new()
|
||||
cleanup_timer.wait_time = 1.0
|
||||
cleanup_timer.one_shot = true
|
||||
cleanup_timer.connect("timeout", clean_up)
|
||||
add_child(cleanup_timer)
|
||||
cleanup_timer.start()
|
||||
|
||||
func clean_up() -> void:
|
||||
print("\n=== All tests completed ===")
|
||||
print("Cutscene system is working correctly!")
|
||||
Reference in New Issue
Block a user