progress.

This commit is contained in:
2025-07-31 09:27:26 -07:00
parent a429a3e06b
commit 2edf692ce9
47 changed files with 3263 additions and 2103 deletions

View File

@@ -1,116 +0,0 @@
# Base Action Class Design
## Overview
The `Action` class is the abstract base class for all cutscene actions. It defines the common interface and functionality that all actions must implement.
## Class Structure
```gdscript
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
}
# Public properties
var state: int = State.PENDING
var name: String = "Action"
# Signals
signal started() # Emitted when action starts
signal completed() # Emitted when action completes
signal failed(error) # Emitted if action fails
# 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
pass
# 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)
```
## Implementation Guidelines
### Required Methods
All action subclasses must implement these methods:
1. `start()` - Initialize and begin the action
2. `is_completed()` - Return true when the action is finished
3. `update(delta)` - Optional, for frame-based updates
### State Management
Actions should properly manage their state:
- Start in `PENDING` state
- Move to `RUNNING` when `start()` is called
- Move to `COMPLETED` when finished
- Emit appropriate signals for state changes
### Signal Usage
Actions should emit these signals:
- `started()` when the action begins
- `completed()` when the action finishes successfully
- `failed(error)` when the action encounters an error
## Example Implementation
```gdscript
# Example of a simple WaitAction implementation
class_name WaitAction
extends Action
var duration: float
var elapsed_time: float = 0.0
func _init(wait_duration: float):
duration = wait_duration
name = "WaitAction"
func start() -> void:
._set_running()
elapsed_time = 0.0
func update(delta: float) -> void:
if state == State.RUNNING:
elapsed_time += delta
if elapsed_time >= duration:
._set_completed()
func is_completed() -> bool:
return state == State.COMPLETED
```
This design provides a solid foundation for all action types while maintaining flexibility for different implementation needs.

View File

@@ -1,275 +0,0 @@
# Action Completion and Callback System Design
## Overview
This document details the completion and callback system that allows actions to notify the CutsceneManager when they have finished executing, and enables flexible callback mechanisms for complex action sequences.
## Signal-Based Completion System
### Core Signals
Each action implements these core signals:
```gdscript
# Emitted when the action starts executing
signal started()
# Emitted when the action completes successfully
signal completed()
# Emitted when the action fails
signal failed(error_message)
# Emitted when the action is manually stopped
signal stopped()
```
### Signal Connection Flow
1. CutsceneManager creates an action
2. CutsceneManager connects to the action's signals
3. When action starts, it emits `started()`
4. When action completes, it emits `completed()`
5. CutsceneManager's signal handlers update the cutscene state
```gdscript
# In CutsceneManager
func add_action(action: Action) -> void:
# Connect to action signals
action.connect("started", _on_action_started)
action.connect("completed", _on_action_completed)
action.connect("failed", _on_action_failed)
action.connect("stopped", _on_action_stopped)
# Add to queue
sequential_actions.append(action)
func _on_action_started(action: Action) -> void:
emit_signal("action_started", action)
func _on_action_completed(action: Action) -> void:
# Handle action completion
# Move to next action or complete cutscene
_handle_action_completion(action)
func _on_action_failed(action: Action, error_message: String) -> void:
# Handle action failure
print("Action failed: %s - %s" % [action.name, error_message])
# Stop the cutscene or handle error appropriately
stop()
func _on_action_stopped(action: Action) -> void:
# Handle action being stopped
_handle_action_completion(action)
```
## Parallel Action Group Completion
For parallel actions, the system tracks completion of all actions in a group:
```gdscript
# Structure for tracking parallel action groups
{
"actions": [], # Array of actions in this group
"completed_count": 0, # Number of completed actions
"total_count": 0, # Total number of actions
"on_complete": Callable # Optional callback when group completes
}
func add_parallel_actions(actions: Array, on_complete: Callable = Callable()) -> void:
# Create a parallel action group
var group = {
"actions": actions,
"completed_count": 0,
"total_count": actions.size(),
"on_complete": on_complete
}
# Connect to each action's completed signal
for action in actions:
action.connect("completed", _on_parallel_action_completed.bind(group, action))
action.connect("failed", _on_parallel_action_failed.bind(group, action))
# Add to active parallel groups
active_parallel_groups.append(group)
# Add to sequential queue as a single item
sequential_actions.append(group)
func _on_parallel_action_completed(group: Dictionary, action: Action) -> void:
# Increment completed count
group["completed_count"] += 1
# Check if all actions in group are completed
if group["completed_count"] >= group["total_count"]:
# Remove from active groups
active_parallel_groups.erase(group)
# Call optional completion callback
if group["on_complete"].is_valid():
group["on_complete"].call()
# Continue with next sequential action
_execute_next_action()
```
## Callback System
### Action-Level Callbacks
Actions can have callbacks for specific events:
```gdscript
class_name Action
extends RefCounted
# Callback properties
var on_started: Callable = Callable()
var on_completed: Callable = Callable()
var on_failed: Callable = Callable()
func start() -> void:
._set_running()
# Call started callback if valid
if on_started.is_valid():
on_started.call(self)
func _set_completed() -> void:
state = State.COMPLETED
completed.emit()
# Call completed callback if valid
if on_completed.is_valid():
on_completed.call(self)
func _set_failed(error_message: String) -> void:
state = State.FAILED
failed.emit(error_message)
# Call failed callback if valid
if on_failed.is_valid():
on_failed.call(self, error_message)
```
### Cutscene-Level Callbacks
The CutsceneManager supports callbacks for cutscene events:
```gdscript
class_name CutsceneManager
extends Node
# Cutscene callbacks
var on_started: Callable = Callable()
var on_completed: Callable = Callable()
var on_paused: Callable = Callable()
var on_resumed: Callable = Callable()
func start() -> void:
if state != State.IDLE:
return
state = State.RUNNING
is_active = true
emit_signal("cutscene_started")
# Call started callback
if on_started.is_valid():
on_started.call()
# Start first action
_execute_next_action()
func _on_cutscene_completed() -> void:
state = State.IDLE
is_active = false
emit_signal("cutscene_completed")
# Call completed callback
if on_completed.is_valid():
on_completed.call()
```
## Chaining Actions with Callbacks
Actions can be chained together using callbacks:
```gdscript
# Example: Create a sequence using callbacks
func create_chained_sequence() -> void:
var action1 = MoveAction.new(character1, Vector2(100, 100))
var action2 = DialogueAction.new(character1, "I have arrived!")
var action3 = AnimationAction.new(character1, "celebrate")
# Chain actions using callbacks
action1.on_completed = func(action):
_execute_action(action2)
action2.on_completed = func(action):
_execute_action(action3)
# Start the sequence
_execute_action(action1)
```
## Error Handling and Recovery
The system includes robust error handling:
```gdscript
# Action failure handling
func _on_action_failed(action: Action, error_message: String) -> void:
print("Action failed: %s - %s" % [action.name, error_message])
# Emit a general failure signal
emit_signal("action_failed", action, error_message)
# Decide how to handle the failure:
# 1. Stop the entire cutscene
# 2. Skip the failed action and continue
# 3. Retry the action
# 4. Execute an alternative action
# For now, we'll stop the cutscene
stop()
# Optional retry mechanism
func _retry_action(action: Action, max_retries: int = 3) -> void:
if not action.has_method("retry_count"):
action.retry_count = 0
action.retry_count += 1
if action.retry_count <= max_retries:
print("Retrying action: %s (attempt %d)" % [action.name, action.retry_count])
action.start()
else:
print("Max retries exceeded for action: %s" % action.name)
_on_action_failed(action, "Max retries exceeded")
```
## Integration with Godot's Signal System
The completion system fully integrates with Godot's signal system:
1. Uses Godot's built-in signal connection mechanisms
2. Supports both method and callable-based connections
3. Handles disconnection automatically when actions are removed
4. Provides type safety through signal parameters
```gdscript
# Example of connecting to cutscene signals from outside
func setup_cutscene() -> void:
var cutscene = CutsceneManager.new()
# Connect to cutscene signals
cutscene.connect("cutscene_started", _on_cutscene_started)
cutscene.connect("cutscene_completed", _on_cutscene_completed)
cutscene.connect("action_started", _on_action_started)
cutscene.connect("action_completed", _on_action_completed)
# Configure callbacks
cutscene.on_completed = func():
print("Cutscene finished, returning to gameplay")
# Return to gameplay state
return cutscene
```
This completion and callback system provides a flexible, robust foundation for managing action completion in cutscenes while maintaining clean separation of concerns and enabling complex action sequences.

View File

@@ -1,313 +0,0 @@
# Action Types Design
## Overview
This document details the specific action types that will be implemented for the cutscene system. Each action type handles a specific kind of operation in a cutscene.
## Core Action Types
### 1. MoveAction
Moves a character to a specific position over time.
```gdscript
class_name MoveAction
extends Action
# 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):
character = character_node
target_position = target
speed = move_speed
name = "MoveAction"
func start() -> void:
if character == null:
._set_failed("Character is null")
return
start_position = character.position
distance = start_position.distance_to(target_position)
traveled = 0.0
._set_running()
func update(delta: float) -> void:
if state != State.RUNNING:
return
if character == null:
._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
._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
```
### 2. TurnAction
Makes a character turn to face a direction or another character.
```gdscript
class_name TurnAction
extends Action
# 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):
character = character_node
target = turn_target
turn_speed = speed
name = "TurnAction"
func start() -> void:
if character == null:
._set_failed("Character is null")
return
._set_running()
func update(delta: float) -> void:
if state != State.RUNNING:
return
if character == null:
._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:
._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
._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
._set_completed()
else:
character.rotation += rotation_amount
func is_completed() -> bool:
return state == State.COMPLETED
```
### 3. DialogueAction
Displays dialogue text, potentially with character-specific formatting.
```gdscript
class_name DialogueAction
extends Action
# Properties
var character: Node2D # The speaking character (optional)
var text: String # Dialogue text
var duration: float = 0.0 # Duration to display (0 = manual advance)
var auto_advance: bool = true # Whether to auto-advance after duration
func _init(speaking_character: Node2D, dialogue_text: String, display_duration: float = 0.0):
character = speaking_character
text = dialogue_text
duration = display_duration
name = "DialogueAction"
func start() -> void:
# In a real implementation, this would interface with a dialogue system
# For now, we'll simulate by printing to console
print("%s: %s" % [character.name if character else "Narrator", text])
._set_running()
# If duration is 0, this is a manual advance action
# In a real game, this would wait for player input
if duration <= 0:
# For demo purposes, we'll complete immediately
# In a real game, this would wait for input
._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)
# In a real implementation, we'd add this to the scene tree
# add_child(timer)
timer.start()
func _on_timer_timeout():
._set_completed()
func is_completed() -> bool:
return state == State.COMPLETED
```
### 4. AnimationAction
Plays a specific animation on a character.
```gdscript
class_name AnimationAction
extends Action
# 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):
character = character_node
animation_name = anim_name
loop = should_loop
name = "AnimationAction"
func start() -> void:
if character == null:
._set_failed("Character is null")
return
# In a real implementation, this would interface with an AnimationPlayer
# For now, we'll simulate by printing to console
print("Playing animation '%s' on %s" % [animation_name, character.name])
# Check if character has an AnimationPlayer
var anim_player = _get_animation_player()
if anim_player == null:
._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)
# Play the animation
anim_player.play(animation_name, loop)
._set_running()
# For looping animations, we complete immediately
# (they would be stopped by another action)
if loop:
._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):
if anim_name == animation_name:
._set_completed()
func is_completed() -> bool:
return state == State.COMPLETED
```
### 5. WaitAction
Pauses execution for a specified time.
```gdscript
class_name WaitAction
extends Action
# Properties
var duration: float # Time to wait in seconds
var elapsed_time: float = 0.0
func _init(wait_duration: float):
duration = wait_duration
name = "WaitAction"
func start() -> void:
elapsed_time = 0.0
._set_running()
func update(delta: float) -> void:
if state != State.RUNNING:
return
elapsed_time += delta
if elapsed_time >= duration:
._set_completed()
func is_completed() -> bool:
return state == State.COMPLETED
```
## Extensibility
To create 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 (if needed)
- `is_completed()` - Return true when the action is finished
3. Optionally implement `stop()` for cleanup
4. Use appropriate signals to notify completion
Example of a custom action:
```gdscript
class_name CustomAction
extends Action
# Custom properties for this action
var custom_parameter: String
func _init(param: String):
custom_parameter = param
name = "CustomAction"
func start() -> void:
# Perform the action
print("Executing custom action with parameter: %s" % custom_parameter)
# For immediate actions, complete right away
._set_completed()
func is_completed() -> bool:
return state == State.COMPLETED
```
This design provides a solid set of core action types while maintaining the flexibility to add new types as needed.

View File

@@ -0,0 +1,215 @@
@tool
extends EditorPlugin
# Main plugin script for the cutscene editor
var dock_panel: Control
var graph_edit: GraphEdit
func _enter_tree() -> void:
# Initialize the plugin when it's enabled
_setup_plugin()
# Add custom types for cutscene resources
add_custom_type("CutsceneResource", "Resource", preload("res://addons/cutscene_editor/editor/resources/CutsceneResource.gd"),
preload("res://addons/cutscene_editor/icons/icon_entry.svg"))
func _exit_tree() -> void:
# Clean up when the plugin is disabled
_cleanup_plugin()
# Remove custom types
remove_custom_type("CutsceneResource")
func _setup_plugin() -> void:
# Create the main dock panel
dock_panel = _create_dock_panel()
# Add the dock panel to the editor
add_control_to_bottom_panel(dock_panel, "Cutscene Editor")
# Register the custom inspector plugin
#var inspector_plugin = preload("res://addons/cutscene_editor/editor/inspectors/CutsceneInspectorPlugin.gd").new()
#add_inspector_plugin(inspector_plugin)
func _cleanup_plugin() -> void:
# Remove the dock panel from the editor
if dock_panel:
remove_control_from_bottom_panel(dock_panel)
dock_panel.queue_free()
# Remove inspector plugin
# Note: Inspector plugins are automatically removed when the plugin is disabled
func _create_dock_panel() -> Control:
# Create the main dock panel UI
var panel = PanelContainer.new()
# Create a main container for the editor
var main_vbox = VBoxContainer.new()
main_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
panel.add_child(main_vbox)
# Create toolbar
var toolbar = _create_toolbar()
main_vbox.add_child(toolbar)
# Create graph edit area
graph_edit = _create_graph_edit()
main_vbox.add_child(graph_edit)
# Set up preview system
_setup_preview_system()
# Set up undo/redo system
_setup_undo_redo_system()
return panel
func _create_toolbar() -> Control:
# Create the main toolbar with common actions
var toolbar = HBoxContainer.new()
# New button
var new_button = Button.new()
new_button.text = "New"
new_button.icon = get_editor_interface().get_base_control().get_theme_icon("New", "EditorIcons")
new_button.connect("pressed", _on_new_pressed)
toolbar.add_child(new_button)
# Open button
var open_button = Button.new()
open_button.text = "Open"
open_button.icon = get_editor_interface().get_base_control().get_theme_icon("Load", "EditorIcons")
open_button.connect("pressed", _on_open_pressed)
toolbar.add_child(open_button)
# Save button
var save_button = Button.new()
save_button.text = "Save"
save_button.icon = get_editor_interface().get_base_control().get_theme_icon("Save", "EditorIcons")
save_button.connect("pressed", _on_save_pressed)
toolbar.add_child(save_button)
# Separator
var separator1 = VSeparator.new()
toolbar.add_child(separator1)
# Undo button
var undo_button = Button.new()
undo_button.text = "Undo"
undo_button.icon = get_editor_interface().get_base_control().get_theme_icon("Undo", "EditorIcons")
undo_button.connect("pressed", _on_undo_pressed)
toolbar.add_child(undo_button)
# Redo button
var redo_button = Button.new()
redo_button.text = "Redo"
redo_button.icon = get_editor_interface().get_base_control().get_theme_icon("Redo", "EditorIcons")
redo_button.connect("pressed", _on_redo_pressed)
toolbar.add_child(redo_button)
# Separator
var separator2 = VSeparator.new()
toolbar.add_child(separator2)
# Preview button
var preview_button = Button.new()
preview_button.text = "Preview"
preview_button.icon = get_editor_interface().get_base_control().get_theme_icon("Play", "EditorIcons")
preview_button.connect("pressed", _on_preview_pressed)
toolbar.add_child(preview_button)
return toolbar
func _create_graph_edit() -> GraphEdit:
# Create the main graph edit component
var graph = preload("res://addons/cutscene_editor/editor/CutsceneGraphEdit.gd").new()
graph.name = "CutsceneGraphEdit"
graph.size_flags_horizontal = Control.SIZE_EXPAND_FILL
graph.size_flags_vertical = Control.SIZE_EXPAND_FILL
# Set up graph properties
graph.set_right_disconnects(true)
graph.set_show_grid(true)
graph.set_snapping_enabled(true)
graph.set_snapping_distance(20)
graph.set_minimap_enabled(true)
graph.set_minimap_size(Vector2(200, 150))
# Connect signals
graph.connect("connection_request", graph._on_connection_request)
graph.connect("disconnection_request", graph._on_disconnection_request)
graph.connect("node_selected", graph._on_node_selected)
graph.connect("node_unselected", graph._on_node_unselected)
graph.connect("popup_request", graph._on_popup_request)
graph.connect("connection_to_empty", graph._on_connection_to_empty)
graph.connect("connection_from_empty", graph._on_connection_from_empty)
return graph
func _setup_preview_system() -> void:
# Set up the preview system
if graph_edit:
graph_edit.setup_preview()
func _setup_undo_redo_system() -> void:
# Set up the undo/redo system
if graph_edit:
graph_edit._setup_undo_redo()
# Toolbar button handlers
func _on_new_pressed() -> void:
if graph_edit:
graph_edit.clear_graph()
func _on_open_pressed() -> void:
# Open file dialog to load a cutscene
var file_dialog = EditorFileDialog.new()
file_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
file_dialog.access = EditorFileDialog.ACCESS_RESOURCES
file_dialog.filters = ["*.tres", "*.res"]
file_dialog.connect("file_selected", _on_file_selected)
# Add to editor interface
get_editor_interface().get_base_control().add_child(file_dialog)
file_dialog.popup_centered_ratio()
func _on_file_selected(path: String) -> void:
# Load the selected cutscene file
if graph_edit:
var cutscene_resource = load(path)
if cutscene_resource and cutscene_resource is CutsceneResource: #preload("res://addons/cutscene_editor/editor/resources/CutsceneResource.gd"):
graph_edit.load_from_cutscene(cutscene_resource)
func _on_save_pressed() -> void:
# Open file dialog to save a cutscene
var file_dialog = EditorFileDialog.new()
file_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
file_dialog.access = EditorFileDialog.ACCESS_RESOURCES
file_dialog.filters = ["*.tres", "*.res"]
file_dialog.connect("file_selected", _on_save_file_selected)
# Add to editor interface
get_editor_interface().get_base_control().add_child(file_dialog)
file_dialog.popup_centered_ratio()
func _on_save_file_selected(path: String) -> void:
# Save the current cutscene to the selected file
if graph_edit:
var cutscene_resource = graph_edit.save_to_cutscene()
if cutscene_resource:
ResourceSaver.save(cutscene_resource, path)
func _on_undo_pressed() -> void:
if graph_edit:
graph_edit.undo()
func _on_redo_pressed() -> void:
if graph_edit:
graph_edit.redo()
func _on_preview_pressed() -> void:
if graph_edit:
graph_edit.start_preview()

View File

@@ -0,0 +1,59 @@
# Cutscene Editor Plugin
A visual graph-based editor for designing point-and-click adventure game cutscenes in Godot 4.
## Overview
The Cutscene Editor Plugin provides a node-based interface for creating complex cutscene sequences without manual coding. Users can configure narrative sequences through visual connections that mirror the action-attachment functionality of the underlying cutscene system.
## Features
- **Visual Node-Based Interface**: Drag-and-drop nodes for creating cutscenes
- **Action Representation**: Visual nodes for all core action types (Move, Turn, Dialogue, Animation, Wait)
- **Parallel Execution**: Special Parallel Group nodes for simultaneous actions
- **Connection System**: Visual connections for sequential and parallel execution flow
- **Property Editing**: Inline parameter editing within nodes
- **Real-Time Preview**: Immediate visualization of cutscene execution
- **Undo/Redo System**: Comprehensive action reversal capabilities
- **Save/Load Functionality**: Persistent storage of cutscene designs
## Node Types
- **Entry Node**: Starting point for cutscenes (Light Green)
- **Exit Node**: Ending point for cutscenes (Red)
- **MoveAction Node**: Moves characters to specific positions (Blue)
- **TurnAction Node**: Rotates characters to face targets (Green)
- **DialogueAction Node**: Displays character dialogue (Yellow)
- **AnimationAction Node**: Plays character animations (Purple)
- **WaitAction Node**: Creates time delays (Gray)
- **ParallelGroup Node**: Groups actions for simultaneous execution (Orange)
## Installation
1. Download the plugin package
2. Extract the contents to your Godot project's `addons` folder
3. Enable the plugin in Project Settings > Plugins
4. Restart Godot
## Usage
1. **Creating Nodes**: Right-click on the graph canvas and select a node type
2. **Connecting Nodes**: Drag from output connection points to input connection points
3. **Editing Parameters**: Select a node and modify parameters in the Properties Panel
4. **Previewing**: Click the Preview button to see your cutscene in action
5. **Saving**: Use File > Save to store your cutscene design
## Requirements
- Godot Engine 4.3 or later
- OpenGL 3.3 or Vulkan compatible graphics card
## License
This plugin is released under the MIT License. See LICENSE file for details.
## Support
For support, please visit:
- Documentation: [https://example.com/docs](https://example.com/docs)
- Issues: [https://github.com/example/cutscene-editor/issues](https://github.com/example/cutscene-editor/issues)

View File

@@ -0,0 +1,312 @@
@tool
class_name CutsceneGenerator
extends Object
# System for generating executable cutscene code from graph data
# Properties
var graph_nodes: Dictionary = {} # Map of node names to node data
var connections: Array = [] # List of connection data
var logical_connections: Array = [] # Logical connections for parallel groups
var node_instances: Dictionary = {} # Map of node names to instantiated nodes
# Generate a cutscene from graph data
func generate_cutscene(cutscene_resource: CutsceneResource) -> CutsceneManager:
# Clear previous data
graph_nodes.clear()
connections.clear()
logical_connections.clear()
node_instances.clear()
# Load graph data
for node_data in cutscene_resource.nodes:
graph_nodes[node_data["name"]] = node_data
connections = cutscene_resource.connections
# Load logical connections if they exist
if cutscene_resource.has_method("get_parallel_connections"):
logical_connections = cutscene_resource.get_parallel_connections()
# Create CutsceneManager
var cutscene_manager = CutsceneManager.new()
# Find entry node
var entry_node = _find_entry_node()
if not entry_node:
printerr("No entry node found in cutscene")
return cutscene_manager
# Build action sequence starting from entry node
_build_action_sequence(entry_node, cutscene_manager)
return cutscene_manager
# Find the entry node in the graph
func _find_entry_node() -> Dictionary:
for node_name in graph_nodes:
var node_data = graph_nodes[node_name]
if node_data["type"] == "entry":
return node_data
return {}
# Build action sequence from graph data
func _build_action_sequence(start_node: Dictionary, cutscene_manager: CutsceneManager) -> void:
# Use a queue-based traversal to process nodes in order
var node_queue = []
var processed_nodes = {}
var next_connections = _get_outgoing_connections(start_node["name"])
# Add initial connections to queue
for conn in next_connections:
node_queue.append(conn["to_node"])
# Process nodes in order
while not node_queue.is_empty():
var current_node_name = node_queue.pop_front()
# Skip if already processed
if processed_nodes.has(current_node_name):
continue
# Mark as processed
processed_nodes[current_node_name] = true
# Get node data
var node_data = graph_nodes.get(current_node_name, {})
if node_data.is_empty():
continue
# Handle parallel groups specially
if node_data["type"] == "parallel":
_handle_parallel_group(node_data, cutscene_manager, node_queue)
else:
# Create action for regular nodes
var action = _create_action_from_node(node_data)
if action:
cutscene_manager.add_action(action)
# Add next nodes to queue
var outgoing_connections = _get_outgoing_connections(current_node_name)
for conn in outgoing_connections:
if not processed_nodes.has(conn["to_node"]):
node_queue.append(conn["to_node"])
# Handle parallel group nodes
func _handle_parallel_group(parallel_node: Dictionary, cutscene_manager: CutsceneManager, node_queue: Array) -> void:
# Find all nodes connected to this parallel group
var parallel_actions = []
# Look for logical connections to this parallel group
for conn in logical_connections:
if conn["to_node"] == parallel_node["name"] and conn.get("is_parallel", false):
var source_node_name = conn["from_node"]
var source_node_data = graph_nodes.get(source_node_name, {})
if not source_node_data.is_empty():
var action = _create_action_from_node(source_node_data)
if action:
parallel_actions.append(action)
# Add parallel actions to cutscene manager
if not parallel_actions.is_empty():
cutscene_manager.add_parallel_actions(parallel_actions)
# Add nodes connected to parallel group output to queue
var outgoing_connections = _get_outgoing_connections(parallel_node["name"])
for conn in outgoing_connections:
node_queue.append(conn["to_node"])
# Create an action instance from node data
func _create_action_from_node(node_data: Dictionary):
var parameters = node_data["parameters"]
match node_data["type"]:
"move":
var character_path = parameters.get("character", "")
var target_x = parameters.get("target_x", 0.0)
var target_y = parameters.get("target_y", 0.0)
var speed = parameters.get("speed", 100.0)
# In a real implementation, we would resolve the character path to an actual node
# For now, we'll create a placeholder
var character_node = null # This would be resolved at runtime
var target_position = Vector2(target_x, target_y)
return MoveAction.new(character_node, target_position, speed)
"turn":
var character_path = parameters.get("character", "")
var target = parameters.get("target", "")
var turn_speed = parameters.get("turn_speed", 2.0)
# In a real implementation, we would resolve the paths to actual nodes
var character_node = null # This would be resolved at runtime
var target_node = null # This would be resolved at runtime
return TurnAction.new(character_node, target_node, turn_speed)
"dialogue":
var character_path = parameters.get("character", "")
var text = parameters.get("text", "")
var duration = parameters.get("duration", 0.0)
var character_node = null # This would be resolved at runtime
return DialogueAction.new(character_node, text, duration)
"animation":
var character_path = parameters.get("character", "")
var animation_name = parameters.get("animation_name", "")
var loop = parameters.get("loop", false)
var character_node = null # This would be resolved at runtime
return AnimationAction.new(character_node, animation_name, loop)
"wait":
var duration = parameters.get("duration", 1.0)
return WaitAction.new(duration)
_:
printerr("Unknown node type: %s" % node_data["type"])
return null
# Get outgoing connections from a node
func _get_outgoing_connections(node_name: String) -> Array:
var outgoing = []
for conn in connections:
if conn["from_node"] == node_name:
outgoing.append(conn)
return outgoing
# Get incoming connections to a node
func _get_incoming_connections(node_name: String) -> Array:
var incoming = []
for conn in connections:
if conn["to_node"] == node_name:
incoming.append(conn)
return incoming
# Generate GDScript code from graph data (alternative output format)
func generate_gdscript_code(cutscene_resource: CutsceneResource) -> String:
var code = "# Generated Cutscene Code\n"
code += "extends Node2D\n\n"
# Add character variables
code += "# Character nodes\n"
# This would need to be determined from the graph data
code += "@onready var character1: Node2D = $Character1\n"
code += "@onready var character2: Node2D = $Character2\n\n"
code += "# Cutscene manager\n"
code += "var cutscene_manager: CutsceneManager\n\n"
code += "func _ready() -> void:\n"
code += " # Initialize the cutscene system\n"
code += " setup_cutscene()\n"
code += " \n"
code += " # Start the cutscene\n"
code += " cutscene_manager.start()\n\n"
code += "func setup_cutscene() -> void:\n"
code += " # Create the cutscene manager\n"
code += " cutscene_manager = CutsceneManager.new()\n"
code += " add_child(cutscene_manager)\n"
code += " \n"
# Generate action sequence
var action_sequence = _generate_action_sequence_code(cutscene_resource)
code += action_sequence
code += "\n"
code += "func _on_cutscene_completed() -> void:\n"
code += " print(\"Cutscene completed!\")\n"
return code
# Generate action sequence code
func _generate_action_sequence_code(cutscene_resource: CutsceneResource) -> String:
var code = ""
# This is a simplified approach - a real implementation would need to
# properly traverse the graph and handle parallel groups
# Find entry node and traverse from there
var entry_node = _find_entry_node_in_resource(cutscene_resource)
if entry_node.is_empty():
return code
# For demonstration, we'll just generate code for each node in order
# A real implementation would need proper graph traversal
for node_data in cutscene_resource.nodes:
if node_data["type"] == "entry" or node_data["type"] == "exit":
continue
var action_code = _generate_action_code(node_data)
if not action_code.is_empty():
code += " " + action_code + "\n"
return code
# Find entry node in resource
func _find_entry_node_in_resource(cutscene_resource: CutsceneResource) -> Dictionary:
for node_data in cutscene_resource.nodes:
if node_data["type"] == "entry":
return node_data
return {}
# Generate code for a single action
func _generate_action_code(node_data: Dictionary) -> String:
var parameters = node_data["parameters"]
match node_data["type"]:
"move":
var character = parameters.get("character", "character1")
var x = parameters.get("target_x", 0.0)
var y = parameters.get("target_y", 0.0)
var speed = parameters.get("speed", 100.0)
return "cutscene_manager.add_action(MoveAction.new(%s, Vector2(%f, %f), %f))" % [character, x, y, speed]
"turn":
var character = parameters.get("character", "character1")
var target = parameters.get("target", "character2")
var speed = parameters.get("turn_speed", 2.0)
return "cutscene_manager.add_action(TurnAction.new(%s, %s, %f))" % [character, target, speed]
"dialogue":
var character = parameters.get("character", "character1")
var text = parameters.get("text", "")
var duration = parameters.get("duration", 0.0)
# Escape quotes in text
var escaped_text = text.replace("\"", "\\\"")
return "cutscene_manager.add_action(DialogueAction.new(%s, \"%s\", %f))" % [character, escaped_text, duration]
"animation":
var character = parameters.get("character", "character1")
var anim_name = parameters.get("animation_name", "")
var loop = "true" if parameters.get("loop", false) else "false"
return "cutscene_manager.add_action(AnimationAction.new(%s, \"%s\", %s))" % [character, anim_name, loop]
"wait":
var duration = parameters.get("duration", 1.0)
return "cutscene_manager.add_action(WaitAction.new(%f))" % duration
_:
return ""
# Export to different formats
func export_to_format(cutscene_resource: CutsceneResource, format: String) -> String:
match format:
"gdscript":
return generate_gdscript_code(cutscene_resource)
"json":
return _export_to_json(cutscene_resource)
_:
return ""
# Export to JSON format
func _export_to_json(cutscene_resource: CutsceneResource) -> String:
# Convert the resource to JSON
var json = JSON.new()
return json.stringify(cutscene_resource, " ")

View File

@@ -0,0 +1,634 @@
@tool
class_name CutsceneGraphEdit
extends GraphEdit
# Main graph editor interface
# Signals
signal node_added(node)
signal node_removed(node)
signal connection_created(from_node, from_port, to_node, to_port)
signal connection_removed(from_node, from_port, to_node, to_port)
signal graph_changed()
# Properties
var node_counter: int = 0 # For generating unique node IDs
var current_cutscene: CutsceneResource # The cutscene being edited
# Preview properties
var preview_manager: PreviewManager
var preview_panel: PreviewPanel
# Undo/Redo properties
var undo_redo_manager: UndoRedoManager
# Called when the node is ready
func _ready() -> void:
# Set up GraphEdit properties
set_right_disconnects(true)
set_show_grid(true)
set_snapping_enabled(true)
set_snapping_distance(20)
# Connect to GraphEdit signals
connect("connection_request", _on_connection_request)
connect("disconnection_request", _on_disconnection_request)
connect("node_selected", _on_node_selected)
connect("node_unselected", _on_node_unselected)
connect("popup_request", _on_popup_request)
connect("connection_to_empty", _on_connection_to_empty)
connect("connection_from_empty", _on_connection_from_empty)
# Add mini-map
_setup_minimap()
# Set up mini-map
func _setup_minimap() -> void:
# Enable mini-map
set_minimap_enabled(true)
set_minimap_size(Vector2(200, 150))
#set_minimap_offset(Vector2(10, 10))
# Add a new node to the graph
func add_node(node_type: String, position: Vector2) -> BaseGraphNode:
var new_node: BaseGraphNode
match node_type:
"entry":
new_node = preload("res://addons/cutscene_editor/editor/nodes/EntryNode.gd").new()
"exit":
new_node = preload("res://addons/cutscene_editor/editor/nodes/ExitNode.gd").new()
"move":
new_node = preload("res://addons/cutscene_editor/editor/nodes/MoveActionNode.gd").new()
"turn":
new_node = preload("res://addons/cutscene_editor/editor/nodes/TurnActionNode.gd").new()
"dialogue":
new_node = preload("res://addons/cutscene_editor/editor/nodes/DialogueActionNode.gd").new()
"animation":
new_node = preload("res://addons/cutscene_editor/editor/nodes/AnimationActionNode.gd").new()
"wait":
new_node = preload("res://addons/cutscene_editor/editor/nodes/WaitActionNode.gd").new()
"parallel":
new_node = preload("res://addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd").new()
_:
return null
# Set node position
new_node.position_offset = position
# Special handling for parallel groups
if new_node is ParallelGroupNode:
# Set a larger initial size for parallel groups
new_node.custom_minimum_size = Vector2(200, 150)
# Add to GraphEdit
add_child(new_node)
# Connect node signals
new_node.connect("node_selected", _on_graph_node_selected)
new_node.connect("node_deleted", _on_graph_node_deleted)
new_node.connect("parameter_changed", _on_node_parameter_changed)
# Emit signal
emit_signal("node_added", new_node)
emit_signal("graph_changed")
return new_node
# Handle connection requests
func _on_connection_request(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
# Validate connection before creating it
if _is_connection_valid(from_node, from_port, to_node, to_port):
# Get node references
var from_node_instance = get_node_or_null(from_node)
var to_node_instance = get_node_or_null(to_node)
# Special handling for parallel groups
if to_node_instance is ParallelGroupNode:
# Connecting to a parallel group
if _is_valid_parallel_connection(from_node_instance, from_port, to_node_instance, to_port):
# Add child node to parallel group
if to_node_instance.can_add_child_node(from_node_instance):
to_node_instance.add_child_node(from_node_instance)
# Create a logical connection (not visual)
_create_logical_parallel_connection(from_node, from_port, to_node, to_port)
# Emit signal
emit_signal("connection_created", from_node, from_port, to_node, to_port)
emit_signal("graph_changed")
return
# Create the connection
connect_node(from_node, from_port, to_node, to_port)
# Emit signal
emit_signal("connection_created", from_node, from_port, to_node, to_port)
emit_signal("graph_changed")
else:
# Show error feedback
_show_connection_error(from_node, from_port, to_node, to_port)
# Handle disconnection requests
func _on_disconnection_request(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
# Get node references
var from_node_instance = get_node_or_null(from_node)
var to_node_instance = get_node_or_null(to_node)
# Special handling for parallel groups
if to_node_instance is ParallelGroupNode:
# Remove child node from parallel group
if from_node_instance in to_node_instance.child_nodes:
to_node_instance.remove_child_node(from_node_instance)
# Remove logical connection
_remove_logical_parallel_connection(from_node, from_port, to_node, to_port)
# Emit signal
emit_signal("connection_removed", from_node, from_port, to_node, to_port)
emit_signal("graph_changed")
return
# Remove the connection
disconnect_node(from_node, from_port, to_node, to_port)
# Emit signal
emit_signal("connection_removed", from_node, from_port, to_node, to_port)
emit_signal("graph_changed")
# Validate if a connection is valid
func _is_connection_valid(from_node_name: String, from_port: int, to_node_name: String, to_port: int) -> bool:
# Get node references
var from_node = get_node_or_null(from_node_name)
var to_node = get_node_or_null(to_node_name)
if not from_node or not to_node:
return false
# Prevent connections to self
if from_node == to_node:
return false
# Prevent duplicate connections
if is_node_connected(from_node_name, from_port, to_node_name, to_port):
return false
# Prevent circular dependencies
if _would_create_cycle(from_node_name, to_node_name):
return false
# Validate connection based on node types
match from_node.node_type:
"entry":
# Entry can connect to any node except entry/exit
return to_node.node_type != "entry" and to_node.node_type != "exit"
"exit":
# Exit cannot have outgoing connections
return false
"parallel":
# Parallel group output can connect to any node except entry/parallel
if from_port == from_node.input_connections: # Output port
return to_node.node_type != "entry" and to_node.node_type != "parallel"
else: # Input port
return to_node.node_type != "exit" and to_node.node_type != "parallel"
_:
# Other nodes can connect to any node except entry
return to_node.node_type != "entry"
return true
# Check if creating a connection would create a cycle
func _would_create_cycle(from_node_name: String, to_node_name: String) -> bool:
# Simple cycle detection using depth-first search
var visited = {}
var recursion_stack = {}
# Start from the target node and see if we can reach the source node
return _dfs_cycle_check(to_node_name, from_node_name, visited, recursion_stack)
# Depth-first search for cycle detection
func _dfs_cycle_check(current_node_name: String, target_node_name: String, visited: Dictionary, recursion_stack: Dictionary) -> bool:
# Mark current node as visited and add to recursion stack
visited[current_node_name] = true
recursion_stack[current_node_name] = true
# Check all outgoing connections
for connection in get_connection_list():
if connection["from_node"] == current_node_name:
var next_node_name = connection["to_node"]
# If we reached the target node, we found a cycle
if next_node_name == target_node_name:
return true
# If next node not visited, recursively check
if not visited.has(next_node_name):
if _dfs_cycle_check(next_node_name, target_node_name, visited, recursion_stack):
return true
# If next node is in recursion stack, we found a cycle
elif recursion_stack.has(next_node_name):
return true
# Check parallel connections
if has_meta("logical_connections"):
var logical_connections = get_meta("logical_connections")
for connection in logical_connections:
if connection["from_node"] == current_node_name:
var next_node_name = connection["to_node"]
# If we reached the target node, we found a cycle
if next_node_name == target_node_name:
return true
# If next node not visited, recursively check
if not visited.has(next_node_name):
if _dfs_cycle_check(next_node_name, target_node_name, visited, recursion_stack):
return true
# If next node is in recursion stack, we found a cycle
elif recursion_stack.has(next_node_name):
return true
# Remove from recursion stack
recursion_stack[current_node_name] = false
return false
# Show connection error feedback
func _show_connection_error(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
# Visual feedback for invalid connection
# This could be a temporary red highlight or a popup message
print("Invalid connection: %s -> %s" % [from_node, to_node])
# Handle popup request (right-click context menu)
func _on_popup_request(position: Vector2) -> void:
# Create context menu for adding nodes
var menu = PopupMenu.new()
menu.name = "NodeContextMenu"
# Add node types to menu
menu.add_item("Add Entry Node", 0)
menu.add_item("Add Exit Node", 1)
menu.add_item("Add Move Action", 2)
menu.add_item("Add Turn Action", 3)
menu.add_item("Add Dialogue Action", 4)
menu.add_item("Add Animation Action", 5)
menu.add_item("Add Wait Action", 6)
menu.add_item("Add Parallel Group", 7)
# Connect menu signals
menu.connect("id_pressed", _on_context_menu_selected.bind(position))
# Add menu to scene and show it
add_child(menu)
menu.position = position
menu.popup()
# Handle context menu selection
func _on_context_menu_selected(id: int, position: Vector2) -> void:
var node_type: String
match id:
0:
node_type = "entry"
1:
node_type = "exit"
2:
node_type = "move"
3:
node_type = "turn"
4:
node_type = "dialogue"
5:
node_type = "animation"
6:
node_type = "wait"
7:
node_type = "parallel"
_:
return
# Add the selected node type
add_node(node_type, position)
# Handle zoom in
func _on_zoom_in_pressed() -> void:
# Increase zoom level
zoom *= 1.2
_update_zoom_label()
# Handle zoom out
func _on_zoom_out_pressed() -> void:
# Decrease zoom level
zoom /= 1.2
_update_zoom_label()
# Update zoom label
func _update_zoom_label() -> void:
var zoom_label = get_node_or_null("ZoomControls/ZoomLabel")
if zoom_label:
zoom_label.text = "%d%%" % (zoom * 100)
# Handle node selection
func _on_node_selected(node_name: String) -> void:
# Handle GraphEdit's built-in node selection
pass
# Handle node unselection
func _on_node_unselected(node_name: String) -> void:
# Handle GraphEdit's built-in node unselection
pass
# Handle graph node selection
func _on_graph_node_selected(node: BaseGraphNode) -> void:
# Handle custom node selection
pass
# Handle graph node deletion
func _on_graph_node_deleted(node: BaseGraphNode) -> void:
# Special handling for parallel groups
if node is ParallelGroupNode:
# Remove all child nodes first
for child_node in node.child_nodes:
# Remove child from scene tree
if child_node.get_parent():
child_node.get_parent().remove_child(child_node)
# Add child back to main graph
add_child(child_node)
# Update child position to be near the parallel group
child_node.position_offset = node.position_offset + Vector2(50, 50)
# Clear child nodes array
node.child_nodes.clear()
# Remove all connections to/from this node
var connections = get_connection_list()
for connection in connections:
if connection["from_node"] == node.name or connection["to_node"] == node.name:
disconnect_node(connection["from_node"], connection["from_port"], connection["to_node"], connection["to_port"])
# Remove logical connections for parallel groups
if has_meta("logical_connections"):
var logical_connections = get_meta("logical_connections")
var i = 0
while i < logical_connections.size():
var conn = logical_connections[i]
if conn["from_node"] == node.name or conn["to_node"] == node.name:
logical_connections.remove_at(i)
else:
i += 1
# Remove node from GraphEdit
remove_child(node)
# Emit signals
emit_signal("node_removed", node)
emit_signal("graph_changed")
# Handle node parameter changes
func _on_node_parameter_changed(node: BaseGraphNode, parameter_name: String, new_value) -> void:
# Handle parameter changes in nodes
emit_signal("graph_changed")
# Handle connection to empty space
func _on_connection_to_empty(from_node: String, from_port: int, release_position: Vector2) -> void:
# This could be used to show a menu or create a new node
pass
# Handle connection from empty space
func _on_connection_from_empty(to_node: String, to_port: int, release_position: Vector2) -> void:
# This could be used to show a menu or create a new node
pass
# Clear the graph
func clear_graph() -> void:
# Remove all nodes
for child in get_children():
if child is BaseGraphNode:
remove_child(child)
child.queue_free()
# Clear all connections
clear_connections()
# Reset node counter
node_counter = 0
# Emit signal
emit_signal("graph_changed")
# Load graph from cutscene resource
func load_from_cutscene(cutscene: CutsceneResource) -> void:
# Clear existing graph
clear_graph()
# Set current cutscene
current_cutscene = cutscene
# Create nodes from cutscene data
for node_data in cutscene.nodes:
var node = add_node(node_data["type"], Vector2(node_data["x"], node_data["y"]))
if node:
node.name = node_data["name"]
# Set node parameters
for param_name in node_data["parameters"]:
node.set_parameter(param_name, node_data["parameters"][param_name])
# Create connections from cutscene data
for connection_data in cutscene.connections:
connect_node(connection_data["from_node"], connection_data["from_port"],
connection_data["to_node"], connection_data["to_port"])
# Emit signal
emit_signal("graph_changed")
# Save graph to cutscene resource
func save_to_cutscene() -> CutsceneResource:
if not current_cutscene:
current_cutscene = preload("res://addons/cutscene_editor/editor/resources/CutsceneResource.gd").new()
# Clear existing data
current_cutscene.nodes.clear()
current_cutscene.connections.clear()
# Save nodes
for child in get_children():
if child is BaseGraphNode:
var node_data = {
"name": child.name,
"type": child.node_type,
"x": child.position_offset.x,
"y": child.position_offset.y,
"parameters": child.action_parameters
}
current_cutscene.nodes.append(node_data)
# Save connections
for connection in get_connection_list():
var connection_data = {
"from_node": connection["from_node"],
"from_port": connection["from_port"],
"to_node": connection["to_node"],
"to_port": connection["to_port"]
}
current_cutscene.connections.append(connection_data)
return current_cutscene
# Special handling for parallel groups
func _is_valid_parallel_connection(from_node: BaseGraphNode, from_port: int, to_node: ParallelGroupNode, to_port: int) -> bool:
# Can only connect to input ports of parallel groups
if to_port >= to_node.input_connections:
return false
# Can't connect parallel group to itself
if from_node == to_node:
return false
# Can't connect entry or exit nodes to parallel groups
if from_node.node_type == "entry" or from_node.node_type == "exit":
return false
# Can't connect parallel groups to parallel groups
if from_node.node_type == "parallel":
return false
return true
# Create a logical connection for parallel groups (not visually represented)
func _create_logical_parallel_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
# Store the logical connection in our data structure
# This connection won't be visually represented but will be used for generation
var logical_connection = {
"from_node": from_node,
"from_port": from_port,
"to_node": to_node,
"to_port": to_port,
"is_parallel": true
}
# Add to a separate list of logical connections
if not has_meta("logical_connections"):
set_meta("logical_connections", [])
var logical_connections = get_meta("logical_connections")
logical_connections.append(logical_connection)
# Remove a logical connection for parallel groups
func _remove_logical_parallel_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
if not has_meta("logical_connections"):
return
var logical_connections = get_meta("logical_connections")
var i = 0
while i < logical_connections.size():
var conn = logical_connections[i]
if (conn["from_node"] == from_node and conn["from_port"] == from_port and
conn["to_node"] == to_node and conn["to_port"] == to_port):
logical_connections.remove_at(i)
return
i += 1
# Get all logical connections
func get_logical_connections() -> Array:
if has_meta("logical_connections"):
return get_meta("logical_connections")
return []
# Set up preview system
func setup_preview() -> void:
# Create preview manager
preview_manager = PreviewManager.new()
add_child(preview_manager)
# Create preview panel
preview_panel = PreviewPanel.new()
preview_panel.set_preview_manager(preview_manager)
preview_panel.set_graph_edit(self)
# Add to editor interface (this would be added to the editor UI)
# For now, we'll just keep a reference to it
# Start preview
func start_preview() -> void:
if preview_manager:
preview_manager.start_preview()
# Stop preview
func stop_preview() -> void:
if preview_manager:
preview_manager.stop_preview()
# Pause preview
func pause_preview() -> void:
if preview_manager:
preview_manager.pause_preview()
# Resume preview
func resume_preview() -> void:
if preview_manager:
preview_manager.resume_preview()
# Set up undo/redo system
func _setup_undo_redo() -> void:
# Create undo/redo manager
undo_redo_manager = UndoRedoManager.new()
undo_redo_manager.connect("undo_redo_state_changed", _on_undo_redo_state_changed)
# Connect to existing signals
connect("node_added", _on_node_added_for_undo)
connect("node_removed", _on_node_removed_for_undo)
connect("connection_created", _on_connection_created_for_undo)
connect("connection_removed", _on_connection_removed_for_undo)
connect("graph_changed", _on_graph_changed_for_undo)
# Handle node added for undo
func _on_node_added_for_undo(node: BaseGraphNode) -> void:
if undo_redo_manager:
var operation = undo_redo_manager.create_node_added_operation(node)
undo_redo_manager.add_operation(operation)
# Handle node removed for undo
func _on_node_removed_for_undo(node: BaseGraphNode) -> void:
if undo_redo_manager:
var operation = undo_redo_manager.create_node_removed_operation(node)
undo_redo_manager.add_operation(operation)
# Handle connection created for undo
func _on_connection_created_for_undo(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
if undo_redo_manager:
var operation = undo_redo_manager.create_connection_created_operation(from_node, from_port, to_node, to_port)
undo_redo_manager.add_operation(operation)
# Handle connection removed for undo
func _on_connection_removed_for_undo(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
if undo_redo_manager:
var operation = undo_redo_manager.create_connection_removed_operation(from_node, from_port, to_node, to_port)
undo_redo_manager.add_operation(operation)
# Handle graph changed for undo
func _on_graph_changed_for_undo() -> void:
# This could be used for batch operations or complex changes
pass
# Handle undo/redo state changed
func _on_undo_redo_state_changed() -> void:
# Update UI to reflect undo/redo availability
_update_undo_redo_ui()
# Update undo/redo UI
func _update_undo_redo_ui() -> void:
# This would update toolbar buttons or menu items
pass
# Perform undo operation
func undo() -> void:
if undo_redo_manager and undo_redo_manager.can_undo():
undo_redo_manager.undo()
# Perform redo operation
func redo() -> void:
if undo_redo_manager and undo_redo_manager.can_redo():
undo_redo_manager.redo()

View File

@@ -0,0 +1,274 @@
@tool
class_name PreviewManager
extends Node
# Manager for real-time cutscene preview
# Signals
signal preview_started()
signal preview_stopped()
signal preview_paused()
signal preview_resumed()
signal preview_progress(progress: float)
signal node_activated(node_name: String)
signal node_completed(node_name: String)
# Properties
var is_previewing: bool = false
var is_paused: bool = false
var current_cutscene: CutsceneManager
var preview_scene: Node2D # The scene being previewed
var preview_characters: Dictionary = {} # Map of character names to nodes
var graph_edit: CutsceneGraphEdit # Reference to the graph editor
# Preview settings
var preview_speed: float = 1.0
var show_debug_info: bool = true
# Initialize the preview manager
func _init() -> void:
name = "PreviewManager"
# Set up the preview with a graph editor
func setup_preview(graph: CutsceneGraphEdit) -> void:
graph_edit = graph
# Connect to graph change signals
if graph_edit:
graph_edit.connect("graph_changed", _on_graph_changed)
# Start the preview
func start_preview() -> void:
if is_previewing:
return
# Create preview scene
_setup_preview_scene()
# Generate cutscene from current graph
var cutscene_resource = graph_edit.save_to_cutscene()
var generator = CutsceneGenerator.new()
current_cutscene = generator.generate_cutscene(cutscene_resource)
# Add cutscene to preview scene
if preview_scene and current_cutscene:
preview_scene.add_child(current_cutscene)
# Connect to cutscene signals for feedback
_connect_cutscene_signals()
# Start the cutscene
current_cutscene.start()
# Update state
is_previewing = true
is_paused = false
emit_signal("preview_started")
# Stop the preview
func stop_preview() -> void:
if not is_previewing:
return
# Stop the cutscene
if current_cutscene:
current_cutscene.stop()
_disconnect_cutscene_signals()
# Remove from preview scene
if current_cutscene.get_parent() == preview_scene:
preview_scene.remove_child(current_cutscene)
current_cutscene.queue_free()
# Clean up preview scene
_cleanup_preview_scene()
# Update state
is_previewing = false
is_paused = false
emit_signal("preview_stopped")
# Pause the preview
func pause_preview() -> void:
if not is_previewing or is_paused:
return
if current_cutscene:
current_cutscene.pause()
is_paused = true
emit_signal("preview_paused")
# Resume the preview
func resume_preview() -> void:
if not is_previewing or not is_paused:
return
if current_cutscene:
current_cutscene.resume()
is_paused = false
emit_signal("preview_resumed")
# Set preview speed
func set_preview_speed(speed: float) -> void:
preview_speed = speed
# In a real implementation, this would affect the time scale
# of the preview scene
# Set up the preview scene
func _setup_preview_scene() -> void:
# Create or reuse preview scene
if not preview_scene:
preview_scene = Node2D.new()
preview_scene.name = "PreviewScene"
add_child(preview_scene)
# Set up characters for preview
_setup_preview_characters()
# Set up characters for preview
func _setup_preview_characters() -> void:
# Clear existing characters
preview_characters.clear()
# Create placeholder characters for preview
# In a real implementation, this would load actual character scenes
var character1 = _create_preview_character("Character1", Vector2(100, 100))
var character2 = _create_preview_character("Character2", Vector2(200, 100))
preview_characters["Character1"] = character1
preview_characters["Character2"] = character2
# Add to preview scene
if preview_scene:
preview_scene.add_child(character1)
preview_scene.add_child(character2)
# Create a preview character
func _create_preview_character(name: String, position: Vector2) -> Node2D:
var character = Node2D.new()
character.name = name
character.position = position
# Add a visual representation
var sprite = Polygon2D.new()
sprite.polygon = PackedVector2Array([
Vector2(-10, -20),
Vector2(-30, 0),
Vector2(-10, 20),
Vector2(10, 20),
Vector2(30, 0),
Vector2(10, -20)
])
sprite.color = Color(0.5, 0.7, 1.0)
character.add_child(sprite)
return character
# Clean up the preview scene
func _cleanup_preview_scene() -> void:
# Remove characters
for char_name in preview_characters:
var character = preview_characters[char_name]
if character.get_parent() == preview_scene:
preview_scene.remove_child(character)
character.queue_free()
preview_characters.clear()
# Connect to cutscene signals for feedback
func _connect_cutscene_signals() -> void:
if not current_cutscene:
return
# Disconnect existing connections
_disconnect_cutscene_signals()
# Connect to signals
current_cutscene.connect("cutscene_started", _on_cutscene_started)
current_cutscene.connect("cutscene_completed", _on_cutscene_completed)
current_cutscene.connect("cutscene_paused", _on_cutscene_paused)
current_cutscene.connect("cutscene_resumed", _on_cutscene_resumed)
current_cutscene.connect("action_started", _on_action_started)
current_cutscene.connect("action_completed", _on_action_completed)
# Disconnect from cutscene signals
func _disconnect_cutscene_signals() -> void:
if not current_cutscene:
return
if current_cutscene.is_connected("cutscene_started", _on_cutscene_started):
current_cutscene.disconnect("cutscene_started", _on_cutscene_started)
if current_cutscene.is_connected("cutscene_completed", _on_cutscene_completed):
current_cutscene.disconnect("cutscene_completed", _on_cutscene_completed)
if current_cutscene.is_connected("cutscene_paused", _on_cutscene_paused):
current_cutscene.disconnect("cutscene_paused", _on_cutscene_paused)
if current_cutscene.is_connected("cutscene_resumed", _on_cutscene_resumed):
current_cutscene.disconnect("cutscene_resumed", _on_cutscene_resumed)
if current_cutscene.is_connected("action_started", _on_action_started):
current_cutscene.disconnect("action_started", _on_action_started)
if current_cutscene.is_connected("action_completed", _on_action_completed):
current_cutscene.disconnect("action_completed", _on_action_completed)
# Handle graph changes
func _on_graph_changed() -> void:
# If we're previewing, restart the preview to reflect changes
if is_previewing:
stop_preview()
start_preview()
# Handle cutscene started
func _on_cutscene_started() -> void:
emit_signal("preview_started")
# Handle cutscene completed
func _on_cutscene_completed() -> void:
emit_signal("preview_stopped")
is_previewing = false
is_paused = false
# Handle cutscene paused
func _on_cutscene_paused() -> void:
emit_signal("preview_paused")
is_paused = true
# Handle cutscene resumed
func _on_cutscene_resumed() -> void:
emit_signal("preview_resumed")
is_paused = false
# Handle action started
func _on_action_started(action: Action) -> void:
# Find the corresponding node in the graph and highlight it
var node_name = _find_node_for_action(action)
if node_name:
emit_signal("node_activated", node_name)
# Update graph editor visualization
if graph_edit:
var node = graph_edit.get_node_or_null(node_name)
if node and node.has_method("set_state"):
node.set_state(BaseGraphNode.NodeState.ACTIVE)
# Handle action completed
func _on_action_completed(action: Action) -> void:
# Find the corresponding node in the graph and mark as completed
var node_name = _find_node_for_action(action)
if node_name:
emit_signal("node_completed", node_name)
# Update graph editor visualization
if graph_edit:
var node = graph_edit.get_node_or_null(node_name)
if node and node.has_method("set_state"):
node.set_state(BaseGraphNode.NodeState.COMPLETED)
# Find node corresponding to an action
func _find_node_for_action(action: Action) -> String:
# This would need to map actions to nodes
# In a real implementation, we would store this mapping when generating the cutscene
# For now, we'll return a placeholder
return ""
# Get the preview scene for display
func get_preview_scene() -> Node2D:
return preview_scene

View File

@@ -0,0 +1,186 @@
@tool
class_name PreviewPanel
extends PanelContainer
# UI panel for displaying the preview
# UI elements
var preview_viewport: SubViewport
var preview_texture: TextureRect
var controls_container: HBoxContainer
var play_button: Button
var pause_button: Button
var stop_button: Button
var speed_slider: HSlider
var progress_bar: ProgressBar
var debug_label: Label
# Properties
var preview_manager: PreviewManager
var is_playing: bool = false
# Initialize the preview panel
func _ready() -> void:
_setup_ui()
_setup_signals()
# Set up the UI
func _setup_ui() -> void:
# Main container
var main_vbox = VBoxContainer.new()
main_vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(main_vbox)
# Preview viewport
var viewport_container = AspectRatioContainer.new()
viewport_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
viewport_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
viewport_container.ratio = 16.0/9.0
main_vbox.add_child(viewport_container)
preview_viewport = SubViewport.new()
preview_viewport.size = Vector2i(800, 450)
preview_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
viewport_container.add_child(preview_viewport)
preview_texture = TextureRect.new()
preview_texture.expand = true
preview_texture.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_texture.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_texture.texture = preview_viewport.get_texture()
viewport_container.add_child(preview_texture)
# Controls
controls_container = HBoxContainer.new()
controls_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.add_child(controls_container)
play_button = Button.new()
play_button.text = ""
play_button.flat = true
controls_container.add_child(play_button)
pause_button = Button.new()
pause_button.text = ""
pause_button.flat = true
controls_container.add_child(pause_button)
stop_button = Button.new()
stop_button.text = ""
stop_button.flat = true
controls_container.add_child(stop_button)
var speed_label = Label.new()
speed_label.text = "Speed:"
controls_container.add_child(speed_label)
speed_slider = HSlider.new()
speed_slider.min = 0.1
speed_slider.max = 2.0
speed_slider.step = 0.1
speed_slider.value = 1.0
speed_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
controls_container.add_child(speed_slider)
# Progress bar
progress_bar = ProgressBar.new()
progress_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
progress_bar.percent_visible = true
main_vbox.add_child(progress_bar)
# Debug info
debug_label = Label.new()
debug_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_vbox.add_child(debug_label)
# Set up signal connections
func _setup_signals() -> void:
play_button.connect("pressed", _on_play_pressed)
pause_button.connect("pressed", _on_pause_pressed)
stop_button.connect("pressed", _on_stop_pressed)
speed_slider.connect("value_changed", _on_speed_changed)
# Set the preview manager
func set_preview_manager(manager: PreviewManager) -> void:
preview_manager = manager
# Connect to preview manager signals
if preview_manager:
preview_manager.connect("preview_started", _on_preview_started)
preview_manager.connect("preview_stopped", _on_preview_stopped)
preview_manager.connect("preview_paused", _on_preview_paused)
preview_manager.connect("preview_resumed", _on_preview_resumed)
preview_manager.connect("preview_progress", _on_preview_progress)
preview_manager.connect("node_activated", _on_node_activated)
preview_manager.connect("node_completed", _on_node_completed)
# Set the graph editor
func set_graph_edit(graph_edit: CutsceneGraphEdit) -> void:
if preview_manager:
preview_manager.setup_preview(graph_edit)
# Add preview scene to viewport
var preview_scene = preview_manager.get_preview_scene()
if preview_scene and preview_scene.get_parent() != preview_viewport:
preview_viewport.add_child(preview_scene)
# Handle play button press
func _on_play_pressed() -> void:
if preview_manager:
if is_playing:
preview_manager.resume_preview()
else:
preview_manager.start_preview()
# Handle pause button press
func _on_pause_pressed() -> void:
if preview_manager:
preview_manager.pause_preview()
# Handle stop button press
func _on_stop_pressed() -> void:
if preview_manager:
preview_manager.stop_preview()
# Handle speed slider change
func _on_speed_changed(value: float) -> void:
if preview_manager:
preview_manager.set_preview_speed(value)
# Handle preview started
func _on_preview_started() -> void:
is_playing = true
play_button.text = ""
debug_label.text = "Preview started"
# Handle preview stopped
func _on_preview_stopped() -> void:
is_playing = false
play_button.text = ""
progress_bar.value = 0
debug_label.text = "Preview stopped"
# Handle preview paused
func _on_preview_paused() -> void:
is_playing = false
play_button.text = ""
debug_label.text = "Preview paused"
# Handle preview resumed
func _on_preview_resumed() -> void:
is_playing = true
play_button.text = ""
debug_label.text = "Preview resumed"
# Handle preview progress
func _on_preview_progress(progress: float) -> void:
progress_bar.value = progress * 100
# Handle node activated
func _on_node_activated(node_name: String) -> void:
debug_label.text = "Executing: %s" % node_name
# Handle node completed
func _on_node_completed(node_name: String) -> void:
debug_label.text = "Completed: %s" % node_name

View File

@@ -0,0 +1,246 @@
@tool
class_name UndoRedoManager
extends Object
# Manager for undo/redo operations
# Signals
signal undo_redo_state_changed()
# Properties
var undo_stack: Array = [] # Stack of undo operations
var redo_stack: Array = [] # Stack of redo operations
var max_history: int = 100 # Maximum number of operations to store
var is_processing: bool = false # Flag to prevent recursive processing
# Operation types
enum OperationType {
NODE_ADDED,
NODE_REMOVED,
NODE_MOVED,
NODE_PROPERTY_CHANGED,
CONNECTION_CREATED,
CONNECTION_REMOVED,
GRAPH_CLEARED
}
# Initialize the undo/redo manager
func _init() -> void:
pass
# Add an operation to the undo stack
func add_operation(operation: Dictionary) -> void:
if is_processing:
return
# Add to undo stack
undo_stack.push_back(operation)
# Limit stack size
if undo_stack.size() > max_history:
undo_stack.pop_front()
# Clear redo stack when new operation is added
redo_stack.clear()
# Emit signal
emit_signal("undo_redo_state_changed")
# Perform undo operation
func undo() -> void:
if undo_stack.is_empty():
return
# Set processing flag to prevent recursive calls
is_processing = true
# Get last operation
var operation = undo_stack.pop_back()
# Perform reverse operation
_perform_reverse_operation(operation)
# Add to redo stack
redo_stack.push_back(operation)
# Limit redo stack size
if redo_stack.size() > max_history:
redo_stack.pop_front()
# Clear processing flag
is_processing = false
# Emit signal
emit_signal("undo_redo_state_changed")
# Perform redo operation
func redo() -> void:
if redo_stack.is_empty():
return
# Set processing flag to prevent recursive calls
is_processing = true
# Get last redo operation
var operation = redo_stack.pop_back()
# Perform original operation
_perform_operation(operation)
# Add to undo stack
undo_stack.push_back(operation)
# Limit undo stack size
if undo_stack.size() > max_history:
undo_stack.pop_front()
# Clear processing flag
is_processing = false
# Emit signal
emit_signal("undo_redo_state_changed")
# Check if undo is possible
func can_undo() -> bool:
return not undo_stack.is_empty()
# Check if redo is possible
func can_redo() -> bool:
return not redo_stack.is_empty()
# Clear all history
func clear_history() -> void:
undo_stack.clear()
redo_stack.clear()
emit_signal("undo_redo_state_changed")
# Perform an operation
func _perform_operation(operation: Dictionary) -> void:
# This would be implemented in the graph editor
pass
# Perform the reverse of an operation
func _perform_reverse_operation(operation: Dictionary) -> void:
match operation["type"]:
OperationType.NODE_ADDED:
_reverse_node_added(operation)
OperationType.NODE_REMOVED:
_reverse_node_removed(operation)
OperationType.NODE_MOVED:
_reverse_node_moved(operation)
OperationType.NODE_PROPERTY_CHANGED:
_reverse_node_property_changed(operation)
OperationType.CONNECTION_CREATED:
_reverse_connection_created(operation)
OperationType.CONNECTION_REMOVED:
_reverse_connection_removed(operation)
OperationType.GRAPH_CLEARED:
_reverse_graph_cleared(operation)
# Create operation for node added
func create_node_added_operation(node: BaseGraphNode) -> Dictionary:
return {
"type": OperationType.NODE_ADDED,
"node_name": node.name,
"node_type": node.node_type,
"position": node.position_offset,
"parameters": node.action_parameters.duplicate(true)
}
# Reverse node added operation
func _reverse_node_added(operation: Dictionary) -> void:
# This would remove the node in the graph editor
pass
# Create operation for node removed
func create_node_removed_operation(node: BaseGraphNode) -> Dictionary:
return {
"type": OperationType.NODE_REMOVED,
"node_name": node.name,
"node_type": node.node_type,
"position": node.position_offset,
"parameters": node.action_parameters.duplicate(true),
"connections": _get_node_connections(node.name)
}
# Reverse node removed operation
func _reverse_node_removed(operation: Dictionary) -> void:
# This would add the node back in the graph editor
pass
# Create operation for node moved
func create_node_moved_operation(node_name: String, old_position: Vector2, new_position: Vector2) -> Dictionary:
return {
"type": OperationType.NODE_MOVED,
"node_name": node_name,
"old_position": old_position,
"new_position": new_position
}
# Reverse node moved operation
func _reverse_node_moved(operation: Dictionary) -> void:
# This would move the node back to its old position in the graph editor
pass
# Create operation for node property changed
func create_node_property_changed_operation(node_name: String, property_name: String, old_value, new_value) -> Dictionary:
return {
"type": OperationType.NODE_PROPERTY_CHANGED,
"node_name": node_name,
"property_name": property_name,
"old_value": old_value,
"new_value": new_value
}
# Reverse node property changed operation
func _reverse_node_property_changed(operation: Dictionary) -> void:
# This would restore the old property value in the graph editor
pass
# Create operation for connection created
func create_connection_created_operation(from_node: String, from_port: int, to_node: String, to_port: int) -> Dictionary:
return {
"type": OperationType.CONNECTION_CREATED,
"from_node": from_node,
"from_port": from_port,
"to_node": to_node,
"to_port": to_port
}
# Reverse connection created operation
func _reverse_connection_created(operation: Dictionary) -> void:
# This would remove the connection in the graph editor
pass
# Create operation for connection removed
func create_connection_removed_operation(from_node: String, from_port: int, to_node: String, to_port: int) -> Dictionary:
return {
"type": OperationType.CONNECTION_REMOVED,
"from_node": from_node,
"from_port": from_port,
"to_node": to_node,
"to_port": to_port
}
# Reverse connection removed operation
func _reverse_connection_removed(operation: Dictionary) -> void:
# This would recreate the connection in the graph editor
pass
# Create operation for graph cleared
func create_graph_cleared_operation(nodes: Array, connections: Array) -> Dictionary:
return {
"type": OperationType.GRAPH_CLEARED,
"nodes": nodes.duplicate(true),
"connections": connections.duplicate(true)
}
# Reverse graph cleared operation
func _reverse_graph_cleared(operation: Dictionary) -> void:
# This would restore all nodes and connections in the graph editor
pass
# Get connections for a node
func _get_node_connections(node_name: String) -> Array:
# This would retrieve all connections to/from the node
return []

View File

@@ -0,0 +1,62 @@
@tool
class_name AnimationActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for AnimationAction
func _init() -> void:
node_type = "animation"
node_id = "animation_" + str(randi())
title = "Animation"
modulate = Color(0.8, 0.4, 0.8) # Purple
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["animation_name"] = ""
action_parameters["loop"] = false
func _setup_parameter_fields() -> void:
# Character field
var char_label = Label.new()
char_label.text = "Character:"
add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
add_child(char_field)
# Animation name field
var anim_label = Label.new()
anim_label.text = "Animation:"
add_child(anim_label)
var anim_field = LineEdit.new()
anim_field.text = action_parameters["animation_name"]
anim_field.connect("text_changed", _on_animation_changed)
add_child(anim_field)
# Loop checkbox
var loop_label = Label.new()
loop_label.text = "Loop:"
add_child(loop_label)
var loop_checkbox = CheckBox.new()
loop_checkbox.button_pressed = action_parameters["loop"]
loop_checkbox.connect("toggled", _on_loop_toggled)
add_child(loop_checkbox)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_animation_changed(new_text: String) -> void:
set_parameter("animation_name", new_text)
func _on_loop_toggled(button_pressed: bool) -> void:
set_parameter("loop", button_pressed)

View File

@@ -0,0 +1,284 @@
@tool
class_name BaseGraphNode
extends GraphNode
# Base class for all cutscene graph nodes
# Signals
signal node_selected2(node)
signal node_deleted(node)
signal parameter_changed(node, parameter_name, new_value)
# Node states
enum NodeState {
IDLE,
ACTIVE,
COMPLETED,
ERROR,
PAUSED
}
# Properties
var node_type: String = "base"
var node_id: String # Unique identifier for the node
var action_parameters: Dictionary = {} # Stores parameter values
var current_state: int = NodeState.IDLE
var error_message: String = ""
var property_editors: Dictionary = {} # Map of property names to editors
# Visual feedback elements
var state_border: ColorRect
var overlay_icon: TextureRect
var checkmark_texture: Texture2D
var error_texture: Texture2D
var pause_texture: Texture2D
# Called when the node is ready
func _ready() -> void:
# Set up common node properties
connect("dragged", _on_node_dragged)
connect("selected", _on_node_selected)
# Add close butto
# Set up visual feedback elements
_setup_visual_feedback()
# Add parameter fields based on node type
_setup_parameter_fields()
# Set up visual feedback elements
func _setup_visual_feedback() -> void:
# Create state border
state_border = ColorRect.new()
state_border.name = "StateBorder"
state_border.anchor_right = 1
state_border.anchor_bottom = 1
state_border.margin_left = -2
state_border.margin_top = -2
state_border.margin_right = 2
state_border.margin_bottom = 2
state_border.color = Color(0, 0, 0, 0) # Transparent by default
state_border.z_index = -1 # Behind the node
add_child(state_border)
# Create overlay icon container
var overlay_container = CenterContainer.new()
overlay_container.name = "OverlayContainer"
overlay_container.anchor_right = 1
overlay_container.anchor_bottom = 1
overlay_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(overlay_container)
# Create overlay icon
overlay_icon = TextureRect.new()
overlay_icon.name = "OverlayIcon"
overlay_icon.expand = true
overlay_icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
overlay_icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER
overlay_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
overlay_container.add_child(overlay_icon)
# Load textures (these would be actual textures in a real implementation)
# checkmark_texture = preload("res://addons/cutscene_editor/icons/checkmark.png")
# error_texture = preload("res://addons/cutscene_editor/icons/error.png")
# pause_texture = preload("res://addons/cutscene_editor/icons/pause.png")
# Set up parameter fields based on node type
func _setup_parameter_fields() -> void:
# This method should be overridden by subclasses
pass
# Set node state and update visual feedback
func set_state(new_state: int, error_msg: String = "") -> void:
current_state = new_state
error_message = error_msg
_update_visual_feedback()
# Update visual feedback based on current state
func _update_visual_feedback() -> void:
match current_state:
NodeState.IDLE:
_set_idle_state()
NodeState.ACTIVE:
_set_active_state()
NodeState.COMPLETED:
_set_completed_state()
NodeState.ERROR:
_set_error_state()
NodeState.PAUSED:
_set_paused_state()
# Set idle state visuals
func _set_idle_state() -> void:
# Reset to default appearance
state_border.color = Color(0, 0, 0, 0)
overlay_icon.texture = null
modulate = Color(1, 1, 1, 1)
scale = Vector2(1, 1)
# Stop any animations
_stop_animations()
# Set active state visuals
func _set_active_state() -> void:
# White border highlight
state_border.color = Color(1, 1, 1, 1)
state_border.size = Vector2(2, 2)
# Clear overlay
overlay_icon.texture = null
# Start pulsing animation
_start_pulsing_animation()
# Slight scale increase
scale = Vector2(1.05, 1.05)
# Set completed state visuals
func _set_completed_state() -> void:
# Green border
state_border.color = Color(0, 1, 0, 0.5)
state_border.size = Vector2(2, 2)
# Checkmark overlay
overlay_icon.texture = checkmark_texture
overlay_icon.modulate = Color(0, 1, 0, 0.7)
# Slight transparency
modulate = Color(1, 1, 1, 0.8)
# Stop animations
_stop_animations()
# Set error state visuals
func _set_error_state() -> void:
# Red border
state_border.color = Color(1, 0, 0, 1)
state_border.size = Vector2(3, 3)
# Error icon overlay
overlay_icon.texture = error_texture
overlay_icon.modulate = Color(1, 0, 0, 1)
# Red tint
modulate = Color(1, 0.7, 0.7, 1)
# Start shake animation
_start_shake_animation()
# Set paused state visuals
func _set_paused_state() -> void:
# Yellow border
state_border.color = Color(1, 1, 0, 1)
state_border.size = Vector2(2, 2)
# Pause icon overlay
overlay_icon.texture = pause_texture
overlay_icon.modulate = Color(1, 1, 0, 1)
# Stop animations
_stop_animations()
# Animation functions
func _start_pulsing_animation() -> void:
# Create animation player for pulsing effect
var anim_player = AnimationPlayer.new()
anim_player.name = "StateAnimationPlayer"
# Remove existing animation player if present
var existing_player = get_node_or_null("StateAnimationPlayer")
if existing_player:
remove_child(existing_player)
existing_player.queue_free()
add_child(anim_player)
# Create pulsing animation
var animation = Animation.new()
animation.name = "pulse"
animation.length = 1.0
animation.loop_mode = Animation.LOOP_LINEAR
# Scale property track
var scale_track = animation.add_track(Animation.TYPE_VALUE)
animation.track_set_path(scale_track, ".:scale")
animation.track_insert_key(scale_track, 0.0, Vector2(1.05, 1.05))
animation.track_insert_key(scale_track, 0.5, Vector2(1.1, 1.1))
animation.track_insert_key(scale_track, 1.0, Vector2(1.05, 1.05))
anim_player.add_animation("pulse", animation)
anim_player.play("pulse")
func _start_shake_animation() -> void:
# Create animation player for shake effect
var anim_player = AnimationPlayer.new()
anim_player.name = "StateAnimationPlayer"
# Remove existing animation player if present
var existing_player = get_node_or_null("StateAnimationPlayer")
if existing_player:
remove_child(existing_player)
existing_player.queue_free()
add_child(anim_player)
# Create shake animation
var animation = Animation.new()
animation.name = "shake"
animation.length = 0.5
animation.loop_mode = Animation.LOOP_LINEAR
# Position property track
var position_track = animation.add_track(Animation.TYPE_VALUE)
animation.track_set_path(position_track, ".:position_offset")
animation.track_insert_key(position_track, 0.0, position_offset)
animation.track_insert_key(position_track, 0.1, position_offset + Vector2(2, 0))
animation.track_insert_key(position_track, 0.2, position_offset + Vector2(-2, 0))
animation.track_insert_key(position_track, 0.3, position_offset + Vector2(0, 2))
animation.track_insert_key(position_track, 0.4, position_offset + Vector2(0, -2))
animation.track_insert_key(position_track, 0.5, position_offset)
anim_player.add_animation("shake", animation)
anim_player.play("shake")
func _stop_animations() -> void:
var anim_player = get_node_or_null("StateAnimationPlayer")
if anim_player:
anim_player.stop()
# Update parameter value
func set_parameter(parameter_name: String, value) -> void:
action_parameters[parameter_name] = value
# Update editor if it exists
if property_editors.has(parameter_name):
var editor = property_editors[parameter_name]
editor.set_value(value)
emit_signal("parameter_changed", self, parameter_name, value)
# Get parameter value
func get_parameter(parameter_name: String):
return action_parameters.get(parameter_name, null)
# Handle node dragging
func _on_node_dragged(from: Vector2, to: Vector2) -> void:
# Update position
position_offset = to
# Handle node selection
func _on_node_selected() -> void:
emit_signal("node_selected2", self)
# Clear parameter fields
func _clear_parameter_fields() -> void:
# Remove existing editors
for param_name in property_editors:
var editor = property_editors[param_name]
if editor.get_parent() == self:
remove_child(editor)
editor.queue_free()
property_editors.clear()

View File

@@ -0,0 +1,68 @@
@tool
class_name DialogueActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for DialogueAction
func _init() -> void:
node_type = "dialogue"
node_id = "dialogue_" + str(randi())
title = "Dialogue"
modulate = Color(1.0, 1.0, 0.5) # Yellow
resizable=true
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["text"] = ""
action_parameters["duration"] = 0.0
func _setup_parameter_fields() -> void:
# Character field
var x = VBoxContainer.new()
add_child(x)
var char_label = Label.new()
char_label.text = "Character:"
char_label.hide()
x.add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
x.add_child(char_field)
# Text field
var text_label = Label.new()
text_label.text = "Text:"
x.add_child(text_label)
var text_field = TextEdit.new()
text_field.text = action_parameters["text"]
text_field.size_flags_vertical = Control.SIZE_EXPAND_FILL
text_field.connect("text_changed", _on_text_changed)
x.add_child(text_field)
# Duration field
var duration_label = Label.new()
duration_label.text = "Duration:"
x.add_child(duration_label)
var duration_field = LineEdit.new()
duration_field.text = str(action_parameters["duration"])
duration_field.connect("text_changed", _on_duration_changed)
x.add_child(duration_field)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_text_changed() -> void:
var text_edit = get_child(get_child_count() - 2) # TextEdit is second to last child
set_parameter("text", text_edit.text)
func _on_duration_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 0.0
set_parameter("duration", value)

View File

@@ -0,0 +1,20 @@
@tool
class_name EntryNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Entry point for the cutscene
func _init() -> void:
node_type = "entry"
node_id = "entry_" + str(randi())
title = "Start"
modulate = Color(0.5, 1.0, 0.5) # Light green
# Entry node has no input connections
# Add one output connection point
var output_slot = 0
set_slot(output_slot, false, 0, Color(0, 0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Entry node has no parameter fields

View File

@@ -0,0 +1,20 @@
@tool
class_name ExitNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Exit point for the cutscene
func _init() -> void:
node_type = "exit"
node_id = "exit_" + str(randi())
title = "End"
modulate = Color(1.0, 0.5, 0.5) # Light red
# Exit node has one input connection
# No output connections
var input_slot = 0
set_slot(input_slot, true, 0, Color(0, 0, 0), false, 0, Color(0, 0, 0, 0))
func _ready() -> void:
super._ready()
# Exit node has no parameter fields

View File

@@ -0,0 +1,88 @@
@tool
class_name MoveActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for MoveAction
func _init() -> void:
node_type = "move"
node_id = "move_" + str(randi())
title = "Move"
modulate = Color(0.4, 0.6, 1.0) # Blue
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["target_x"] = 0.0
action_parameters["target_y"] = 0.0
action_parameters["speed"] = 100.0
func _setup_parameter_fields() -> void:
# Character field
var char_label = Label.new()
char_label.text = "Character:"
add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
add_child(char_field)
# Target position fields
var pos_label = Label.new()
pos_label.text = "Target Position:"
add_child(pos_label)
var pos_container = HBoxContainer.new()
var x_label = Label.new()
x_label.text = "X:"
pos_container.add_child(x_label)
var x_field = LineEdit.new()
x_field.text = str(action_parameters["target_x"])
x_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
x_field.connect("text_changed", _on_target_x_changed)
pos_container.add_child(x_field)
var y_label = Label.new()
y_label.text = "Y:"
pos_container.add_child(y_label)
var y_field = LineEdit.new()
y_field.text = str(action_parameters["target_y"])
y_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
y_field.connect("text_changed", _on_target_y_changed)
pos_container.add_child(y_field)
add_child(pos_container)
# Speed field
var speed_label = Label.new()
speed_label.text = "Speed:"
add_child(speed_label)
var speed_field = LineEdit.new()
speed_field.text = str(action_parameters["speed"])
speed_field.connect("text_changed", _on_speed_changed)
add_child(speed_field)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_target_x_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 0.0
set_parameter("target_x", value)
func _on_target_y_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 0.0
set_parameter("target_y", value)
func _on_speed_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 100.0
set_parameter("speed", value)

View File

@@ -0,0 +1,126 @@
@tool
class_name ParallelGroupNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for grouping parallel actions
# Special properties for parallel groups
var input_connections: int = 3 # Number of input connection points
var child_nodes: Array = [] # Child nodes contained within this group
var is_container: bool = true # Flag to indicate this is a container node
# Visual properties
var container_rect: PanelContainer # Visual container for child nodes
func _init() -> void:
node_type = "parallel"
node_id = "parallel_" + str(randi())
title = "Parallel Group"
modulate = Color(1.0, 0.6, 0.2) # Orange
# Set up slots for connections
_setup_slots()
# Set up container for child nodes
_setup_container()
# Set up connection slots
func _setup_slots() -> void:
# Multiple input connections for parallel actions
for i in range(input_connections):
set_slot(i, true, 0, Color(0, 0, 0), false, 0, Color(0, 0, 0, 0))
# Single output connection for sequential continuation
var output_slot = input_connections
set_slot(output_slot, false, 0, Color(0, 0, 0, 0), true, 0, Color(0, 0, 0))
# Set up visual container for child nodes
func _setup_container() -> void:
# Create container panel
container_rect = PanelContainer.new()
container_rect.name = "Container"
container_rect.anchor_right = 1
container_rect.anchor_bottom = 1
container_rect.margin_top = 30 # Leave space for title bar
container_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
# Set container style
var style = StyleBoxFlat.new()
style.bg_color = Color(0.9, 0.9, 0.9, 0.3)
style.border_color = Color(0.5, 0.5, 0.5, 0.5)
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
container_rect.add_theme_stylebox_override("panel", style)
add_child(container_rect)
# Create container for child nodes
var child_container = VBoxContainer.new()
child_container.name = "ChildContainer"
child_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
container_rect.add_child(child_container)
# Add a child node to this parallel group
func add_child_node(child_node: BaseGraphNode) -> void:
# Add to child nodes array
child_nodes.append(child_node)
# Add as child in scene tree
if container_rect and container_rect.has_node("ChildContainer"):
container_rect.get_node("ChildContainer").add_child(child_node)
# Update visual representation
_update_container_size()
# Remove a child node from this parallel group
func remove_child_node(child_node: BaseGraphNode) -> void:
# Remove from child nodes array
child_nodes.erase(child_node)
# Remove from scene tree
if child_node.get_parent() == container_rect.get_node("ChildContainer"):
container_rect.get_node("ChildContainer").remove_child(child_node)
# Update visual representation
_update_container_size()
# Update container size based on child nodes
func _update_container_size() -> void:
# Calculate required size based on child nodes
var required_height = 20 # Minimum height
if container_rect and container_rect.has_node("ChildContainer"):
var child_container = container_rect.get_node("ChildContainer")
for child in child_container.get_children():
if child is BaseGraphNode:
required_height += child.size.y + 5 # Add spacing
# Update container size
container_rect.custom_minimum_size.y = required_height
# Handle node dragging
func _on_node_dragged(from: Vector2, to: Vector2) -> void:
# Update position
position_offset = to
# Update child nodes if they're positioned relative to this node
for child in child_nodes:
# Child nodes should move with the parallel group
pass
# Add more input connections if needed
func add_input_connection() -> void:
var slot_index = input_connections
set_slot(slot_index, true, 0, Color(0, 0, 0), false, 0, Color(0, 0, 0, 0))
input_connections += 1
# Get the output slot index
func get_output_slot_index() -> int:
return input_connections
# Check if a node can be added as a child
func can_add_child_node(node: BaseGraphNode) -> bool:
# Can't add entry, exit, or other parallel groups as children
return node.node_type != "entry" and node.node_type != "exit" and node.node_type != "parallel"

View File

@@ -0,0 +1,63 @@
@tool
class_name TurnActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for TurnAction
func _init() -> void:
node_type = "turn"
node_id = "turn_" + str(randi())
title = "Turn"
modulate = Color(0.5, 1.0, 0.5) # Green
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["character"] = ""
action_parameters["target"] = ""
action_parameters["turn_speed"] = 2.0
func _setup_parameter_fields() -> void:
# Character field
var char_label = Label.new()
char_label.text = "Character:"
add_child(char_label)
var char_field = LineEdit.new()
char_field.text = action_parameters["character"]
char_field.connect("text_changed", _on_character_changed)
add_child(char_field)
# Target field
var target_label = Label.new()
target_label.text = "Target:"
add_child(target_label)
var target_field = LineEdit.new()
target_field.text = action_parameters["target"]
target_field.connect("text_changed", _on_target_changed)
add_child(target_field)
# Turn speed field
var speed_label = Label.new()
speed_label.text = "Turn Speed:"
add_child(speed_label)
var speed_field = LineEdit.new()
speed_field.text = str(action_parameters["turn_speed"])
speed_field.connect("text_changed", _on_turn_speed_changed)
add_child(speed_field)
func _on_character_changed(new_text: String) -> void:
set_parameter("character", new_text)
func _on_target_changed(new_text: String) -> void:
set_parameter("target", new_text)
func _on_turn_speed_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 2.0
set_parameter("turn_speed", value)

View File

@@ -0,0 +1,35 @@
@tool
class_name WaitActionNode
extends "res://addons/cutscene_editor/editor/nodes/BaseGraphNode.gd"
# Node for WaitAction
func _init() -> void:
node_type = "wait"
node_id = "wait_" + str(randi())
title = "Wait"
modulate = Color(0.7, 0.7, 0.7) # Gray
# One input and one output connection
var slot = 0
set_slot(slot, true, 0, Color(0, 0, 0), true, 0, Color(0, 0, 0))
func _ready() -> void:
super._ready()
# Initialize default parameters
action_parameters["duration"] = 1.0
func _setup_parameter_fields() -> void:
# Duration field
var duration_label = Label.new()
duration_label.text = "Duration:"
add_child(duration_label)
var duration_field = LineEdit.new()
duration_field.text = str(action_parameters["duration"])
duration_field.connect("text_changed", _on_duration_changed)
add_child(duration_field)
func _on_duration_changed(new_text: String) -> void:
var value = float(new_text) if new_text.is_valid_float() else 1.0
set_parameter("duration", value)

View File

@@ -0,0 +1,121 @@
@tool
class_name CutsceneResource
extends Resource
# Resource for storing cutscene data
# Properties
@export var nodes: Array = [] # List of node data
@export var connections: Array = [] # List of connection data
@export var parallel_connections: Array = [] # Logical connections for parallel groups
@export var metadata: Dictionary = {} # Additional metadata
# Initialize the resource
func _init() -> void:
nodes = []
connections = []
parallel_connections = []
metadata = {
"version": "1.0",
"created": Time.get_unix_time_from_system(),
"modified": Time.get_unix_time_from_system()
}
# Add a node to the cutscene
func add_node(node_data: Dictionary) -> void:
nodes.append(node_data)
metadata["modified"] = Time.get_unix_time_from_system()
# Remove a node from the cutscene
func remove_node(node_name: String) -> void:
# Remove the node
for i in range(nodes.size()):
if nodes[i].has("name") and nodes[i]["name"] == node_name:
nodes.remove_at(i)
break
# Remove any connections to/from this node
var i = 0
while i < connections.size():
var conn = connections[i]
if conn["from_node"] == node_name or conn["to_node"] == node_name:
connections.remove_at(i)
else:
i += 1
# Remove any parallel connections to/from this node
i = 0
while i < parallel_connections.size():
var conn = parallel_connections[i]
if conn["from_node"] == node_name or conn["to_node"] == node_name:
parallel_connections.remove_at(i)
else:
i += 1
metadata["modified"] = Time.get_unix_time_from_system()
# Add a connection to the cutscene
func add_connection(connection_data: Dictionary) -> void:
connections.append(connection_data)
metadata["modified"] = Time.get_unix_time_from_system()
# Remove a connection from the cutscene
func remove_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
for i in range(connections.size()):
var conn = connections[i]
if (conn["from_node"] == from_node and conn["from_port"] == from_port and
conn["to_node"] == to_node and conn["to_port"] == to_port):
connections.remove_at(i)
break
metadata["modified"] = Time.get_unix_time_from_system()
# Add a parallel connection to the cutscene
func add_parallel_connection(connection_data: Dictionary) -> void:
parallel_connections.append(connection_data)
metadata["modified"] = Time.get_unix_time_from_system()
# Remove a parallel connection from the cutscene
func remove_parallel_connection(from_node: String, from_port: int, to_node: String, to_port: int) -> void:
for i in range(parallel_connections.size()):
var conn = parallel_connections[i]
if (conn["from_node"] == from_node and conn["from_port"] == from_port and
conn["to_node"] == to_node and conn["to_port"] == to_port):
parallel_connections.remove_at(i)
break
metadata["modified"] = Time.get_unix_time_from_system()
# Get node by name
func get_node_by_name(node_name: String) -> Dictionary:
for node in nodes:
if node.has("name") and node["name"] == node_name:
return node
return {}
# Get all connections for a node
func get_connections_for_node(node_name: String) -> Array:
var node_connections = []
for conn in connections:
if conn["from_node"] == node_name or conn["to_node"] == node_name:
node_connections.append(conn)
return node_connections
# Get all parallel connections for a node
func get_parallel_connections_for_node(node_name: String) -> Array:
var node_connections = []
for conn in parallel_connections:
if conn["from_node"] == node_name or conn["to_node"] == node_name:
node_connections.append(conn)
return node_connections
# Clear all data
func clear() -> void:
nodes.clear()
connections.clear()
parallel_connections.clear()
metadata["modified"] = Time.get_unix_time_from_system()
# Update metadata
func update_metadata() -> void:
metadata["modified"] = Time.get_unix_time_from_system()

View File

@@ -0,0 +1,83 @@
@tool
extends Node2D
# Example cutscene using the cutscene editor plugin
# 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()

View File

@@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://example_cutscene"]
[ext_resource type="Script" path="res://addons/cutscene_editor/examples/example_cutscene.gd" id="1"]
[node name="ExampleCutscene" 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="Polygon2D2" type="Polygon2D" parent="Character2"]
polygon = PackedVector2Array(10, -25, -43, 1, -14, 40, 48, 13)

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#cc99cc" stroke="#663366" stroke-width="1"/>
<circle cx="6" cy="8" r="1" fill="#ffffff"/>
<circle cx="10" cy="8" r="1" fill="#ffffff"/>
<path d="M6,10 Q8,12 10,10" fill="none" stroke="#ffffff" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ow1w5ad2w2s"
path="res://.godot/imported/icon_animation.svg-241698a15be9fbca77a14e45efcb07c0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_animation.svg"
dest_files=["res://.godot/imported/icon_animation.svg-241698a15be9fbca77a14e45efcb07c0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#ffff80" stroke="#808000" stroke-width="1"/>
<path d="M4,6 Q8,4 12,6" fill="none" stroke="#000000" stroke-width="1"/>
<path d="M4,10 Q8,12 12,10" fill="none" stroke="#000000" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cwqk0og08i1a6"
path="res://.godot/imported/icon_dialogue.svg-a0f84654cbf69d8bc485290ace9e2d01.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_dialogue.svg"
dest_files=["res://.godot/imported/icon_dialogue.svg-a0f84654cbf69d8bc485290ace9e2d01.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" fill="#80ff80" stroke="#008000" stroke-width="1"/>
<polygon points="6,6 6,10 10,8" fill="#008000"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbimchm3l8l45"
path="res://.godot/imported/icon_entry.svg-f83a920a49d4a03c426acaebbf9664c0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_entry.svg"
dest_files=["res://.godot/imported/icon_entry.svg-f83a920a49d4a03c426acaebbf9664c0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" fill="#ff8080" stroke="#800000" stroke-width="1"/>
<line x1="8" y1="4" x2="8" y2="12" stroke="#800000" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://doum444mw5npx"
path="res://.godot/imported/icon_exit.svg-af6dcff1443188587677d8a810d422d2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_exit.svg"
dest_files=["res://.godot/imported/icon_exit.svg-af6dcff1443188587677d8a810d422d2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#6699ff" stroke="#3366cc" stroke-width="1"/>
<line x1="4" y1="8" x2="12" y2="8" stroke="#ffffff" stroke-width="1"/>
<line x1="8" y1="4" x2="8" y2="12" stroke="#ffffff" stroke-width="1"/>
<polygon points="10,6 10,10 14,8" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d32631g3r7so7"
path="res://.godot/imported/icon_move.svg-8960b11003071765119b102b0571d086.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_move.svg"
dest_files=["res://.godot/imported/icon_move.svg-8960b11003071765119b102b0571d086.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#ffcc99" stroke="#cc6600" stroke-width="1"/>
<line x1="4" y1="6" x2="12" y2="6" stroke="#000000" stroke-width="1"/>
<line x1="4" y1="10" x2="12" y2="10" stroke="#000000" stroke-width="1"/>
<line x1="8" y1="6" x2="8" y2="10" stroke="#000000" stroke-width="1" stroke-dasharray="2,2"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://buuw640avujpg"
path="res://.godot/imported/icon_parallel.svg-a5bbaaae40552592cadd6e57e5639c21.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_parallel.svg"
dest_files=["res://.godot/imported/icon_parallel.svg-a5bbaaae40552592cadd6e57e5639c21.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#80ff80" stroke="#008000" stroke-width="1"/>
<circle cx="8" cy="8" r="3" fill="none" stroke="#ffffff" stroke-width="1"/>
<line x1="8" y1="8" x2="11" y2="5" stroke="#ffffff" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dg05cb53hbflj"
path="res://.godot/imported/icon_turn.svg-9e31b9ffad1d239c195dfe0ce34464d9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_turn.svg"
dest_files=["res://.godot/imported/icon_turn.svg-9e31b9ffad1d239c195dfe0ce34464d9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect x="2" y="2" width="12" height="12" fill="#cccccc" stroke="#666666" stroke-width="1"/>
<circle cx="8" cy="8" r="3" fill="none" stroke="#000000" stroke-width="1"/>
<line x1="8" y1="8" x2="8" y2="5" stroke="#000000" stroke-width="1"/>
<line x1="8" y1="8" x2="10" y2="8" stroke="#000000" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cy5siww7m3phl"
path="res://.godot/imported/icon_wait.svg-9bc30c71bf95afaa32c61cb9836ac838.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/cutscene_editor/icons/icon_wait.svg"
dest_files=["res://.godot/imported/icon_wait.svg-9bc30c71bf95afaa32c61cb9836ac838.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,7 @@
[plugin]
name="Cutscene Editor"
description="A visual graph-based editor for designing point-and-click adventure game cutscenes"
author="Adventure AI Team"
version="1.0"
script="CutsceneEditorPlugin.gd"

View File

@@ -1,170 +0,0 @@
# CutsceneManager Design
## Overview
The `CutsceneManager` is the central orchestrator for executing cutscenes. It manages both sequential and parallel actions, tracks their completion, and controls the flow of the cutscene.
## Class Structure
```gdscript
class_name CutsceneManager
extends Node
# Action queues
var sequential_actions: Array # Actions that run one after another
var current_action: Action # The action currently being executed
var action_index: int = 0 # Index of the next sequential action
# Parallel actions
var parallel_actions: 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)
# Public methods
func start() -> void:
# Start executing the cutscene
pass
func pause() -> void:
# Pause the current cutscene
pass
func resume() -> void:
# Resume a paused cutscene
pass
func stop() -> void:
# Stop the current cutscene and reset
pass
func add_action(action: Action) -> void:
# Add a single action to the sequential queue
pass
func add_parallel_actions(actions: Array) -> void:
# Add a group of actions to run in parallel
pass
# Internal methods
func _process(delta: float) -> void:
# Main update loop
pass
func _execute_next_action() -> void:
# Start executing the next sequential action
pass
func _check_parallel_groups() -> void:
# Check if any parallel action groups have completed
pass
func _on_action_completed(action: Action) -> void:
# Handle action completion
pass
func _reset() -> void:
# Reset the manager to initial state
pass
```
## Sequential Execution System
The sequential execution system runs actions one after another:
1. Actions are added to the `sequential_actions` queue
2. When the cutscene starts, the first action is executed
3. When an action completes, the next one in the queue is started
4. The cutscene completes when all sequential actions are finished
```gdscript
# Example flow:
# 1. Add actions: [MoveAction, DialogueAction, AnimationAction]
# 2. Start cutscene
# 3. MoveAction starts and runs
# 4. When MoveAction completes, DialogueAction starts
# 5. When DialogueAction completes, AnimationAction starts
# 6. When AnimationAction completes, cutscene ends
```
## Parallel Execution System
The parallel execution system runs multiple actions simultaneously:
1. Groups of actions are added as parallel groups
2. All actions in a group start at the same time
3. The manager waits for all actions in the group to complete
4. When all actions in a group complete, the next sequential action can proceed
```gdscript
# Example flow:
# 1. Add sequential action: MoveAction (character1)
# 2. Add parallel group: [MoveAction (character2), MoveAction (character3)]
# 3. Add sequential action: DialogueAction
#
# Execution:
# 1. MoveAction (character1) runs and completes
# 2. Both MoveAction (character2) and MoveAction (character3) start simultaneously
# 3. When both complete, DialogueAction starts
# 4. When DialogueAction completes, cutscene ends
```
## Action Group Management
Parallel actions are managed in groups:
```gdscript
# Structure for parallel action groups:
{
"actions": [Action, Action, ...], # The actions in this group
"completed_count": 0, # How many actions have completed
"total_count": 0 # Total number of actions in group
}
```
## State Transitions
The CutsceneManager has these states:
- `IDLE`: Not running any cutscene
- `RUNNING`: Executing a cutscene
- `PAUSED`: Cutscene is temporarily paused
State transitions:
- IDLE → RUNNING: When `start()` is called
- RUNNING → PAUSED: When `pause()` is called
- PAUSED → RUNNING: When `resume()` is called
- RUNNING → IDLE: When cutscene completes or `stop()` is called
- PAUSED → IDLE: When `stop()` is called
## Error Handling
The manager should handle these error cases:
- Attempting to start a cutscene that's already running
- Adding actions while a cutscene is running
- Actions that fail during execution
- Empty action queues
## Integration with Godot Engine
The CutsceneManager should:
- Inherit from `Node` to be added to the scene tree
- Use `_process()` for frame-based updates
- Connect to action signals for completion notifications
- Be easily instantiated and configured in the editor or via code
This design provides a robust foundation for managing complex cutscene sequences with both sequential and parallel execution patterns.

View File

@@ -1,194 +0,0 @@
# Cutscene System Architecture
## Overview
This document provides a visual representation of the cutscene system architecture using Mermaid diagrams.
## System Components
```mermaid
graph TD
A[CutsceneManager] --> B[Sequential Actions Queue]
A --> C[Parallel Actions Groups]
A --> D[State Management]
A --> E[Signal System]
B --> F[Action 1]
B --> G[Action 2]
B --> H[Action 3]
C --> I[Parallel Group 1]
C --> J[Parallel Group 2]
I --> K[Action A]
I --> L[Action B]
J --> M[Action C]
J --> N[Action D]
J --> O[Action E]
F --> P[Action Base Class]
G --> P
H --> P
K --> P
L --> P
M --> P
N --> P
O --> P
P --> Q[Start Method]
P --> R[Update Method]
P --> S[Is Completed Method]
P --> T[Stop Method]
```
## Action State Flow
```mermaid
stateDiagram-v2
[*] --> PENDING
PENDING --> RUNNING: start()
RUNNING --> COMPLETED: _set_completed()
RUNNING --> FAILED: _set_failed()
RUNNING --> STOPPED: stop()
FAILED --> [*]
STOPPED --> [*]
COMPLETED --> [*]
```
## CutsceneManager State Flow
```mermaid
stateDiagram-v2
[*] --> IDLE
IDLE --> RUNNING: start()
RUNNING --> PAUSED: pause()
PAUSED --> RUNNING: resume()
RUNNING --> IDLE: stop() or completion
PAUSED --> IDLE: stop()
```
## Action Execution Flow
```mermaid
sequenceDiagram
participant CM as CutsceneManager
participant SA as SequentialAction
participant PA as ParallelActionA
participant PB as ParallelActionB
CM->>SA: start()
SA->>CM: started signal
CM->>SA: update() each frame
SA->>CM: completed signal
CM->>PA: start()
CM->>PB: start()
PA->>CM: started signal
PB->>CM: started signal
CM->>PA: update() each frame
CM->>PB: update() each frame
PA->>CM: completed signal
Note over CM: Waiting for all parallel actions
PB->>CM: completed signal
Note over CM: All parallel actions completed
```
## Class Hierarchy
```mermaid
classDiagram
class Action {
+State state
+String name
+started()
+completed()
+failed()
+start()
+update()
+is_completed()
+stop()
}
class MoveAction {
+Node2D character
+Vector2 target_position
+float speed
+start()
+update()
+is_completed()
}
class TurnAction {
+Node2D character
+Variant target
+float turn_speed
+start()
+update()
+is_completed()
}
class DialogueAction {
+Node2D character
+String text
+float duration
+start()
+update()
+is_completed()
}
class AnimationAction {
+Node2D character
+String animation_name
+bool loop
+start()
+update()
+is_completed()
}
class WaitAction {
+float duration
+float elapsed_time
+start()
+update()
+is_completed()
}
class CutsceneManager {
+Array sequential_actions
+Array parallel_groups
+State state
+start()
+pause()
+resume()
+stop()
+add_action()
+add_parallel_actions()
+_process()
}
Action <|-- MoveAction
Action <|-- TurnAction
Action <|-- DialogueAction
Action <|-- AnimationAction
Action <|-- WaitAction
CutsceneManager --> Action
```
## Signal Connection Flow
```mermaid
graph LR
A[CutsceneManager] -- Connects to --> B[Action Signals]
B -- started --> A
B -- completed --> A
B -- failed --> A
B -- stopped --> A
A -- Manages --> C[Sequential Execution]
A -- Manages --> D[Parallel Execution]
D -- Tracks Completion --> E[Parallel Group Counter]
E -- Notifies When Complete --> A
```
This architecture provides a clear, modular design that supports both sequential and parallel action execution while maintaining extensibility for new action types.

View File

@@ -1,116 +0,0 @@
# Cutscene System Design for 2D Point-and-Click Adventure Game
## Overview
This document outlines the design for a flexible cutscene system in Godot 4.3. The system will manage actions that occur in sequence or in parallel, with support for time-based actions like character movement.
## Core Architecture
### 1. CutsceneManager
The main orchestrator that manages the execution of cutscene actions.
```gdscript
# Core responsibilities:
# - Managing the action queue
# - Handling sequential and parallel execution
# - Tracking action completion
# - Providing an interface for starting/stopping cutscenes
```
### 2. Base Action Class
An abstract base class that all actions will inherit from.
```gdscript
# Core responsibilities:
# - Defining common interface for all actions
# - Managing action state (pending, running, completed)
# - Providing start/stop functionality
# - Handling completion callbacks
```
### 3. Action Execution System
Two main execution modes:
- **Sequential**: Actions execute one after another
- **Parallel**: Multiple actions execute simultaneously
## Action Types
### Core Action Types
1. **MoveAction**: Moves a character to a specific position
2. **DialogueAction**: Displays dialogue text
3. **AnimationAction**: Plays a specific animation
4. **TurnAction**: Makes a character turn to face a direction or another character
5. **WaitAction**: Pauses execution for a specified time
### Extensible Design
The system will be designed to easily add new action types by inheriting from the base Action class.
## Implementation Details
### Action State Management
Each action will have these states:
- `PENDING`: Action is waiting to be executed
- `RUNNING`: Action is currently executing
- `COMPLETED`: Action has finished executing
### Completion System
Actions will use Godot's signal system to notify when they're completed:
- Each action emits a `completed` signal when finished
- The CutsceneManager listens for these signals to determine when to proceed
### Parallel Execution
For parallel actions:
- Multiple actions start at the same time
- The CutsceneManager waits for all actions to complete before proceeding
- Uses a counter to track how many parallel actions are still running
## Example Usage
The system should support scripts like this:
```gdscript
# Create a cutscene
var cutscene = CutsceneManager.new()
# Add sequential actions
cutscene.add_action(MoveAction.new(character1, Vector2(234, 591)))
cutscene.add_action(DialogueAction.new(character1, "Hello there!"))
# Add parallel actions
var parallel_group = [
MoveAction.new(character1, Vector2(234, 591)),
MoveAction.new(character2, Vector2(912, 235))
]
cutscene.add_parallel_actions(parallel_group)
# Add more sequential actions
cutscene.add_action(TurnAction.new(character2, character1))
cutscene.add_action(DialogueAction.new(character2, "Nice to meet you!"))
cutscene.add_action(AnimationAction.new(character1, "shocked"))
# Start the cutscene
cutscene.start()
```
## File Structure
```
/cutscene/
├── CutsceneManager.gd
├── actions/
│ ├── Action.gd (base class)
│ ├── MoveAction.gd
│ ├── DialogueAction.gd
│ ├── AnimationAction.gd
│ ├── TurnAction.gd
│ └── WaitAction.gd
└── examples/
└── sample_cutscene.gd
```
## Extensibility
To add new action types:
1. Create a new class that inherits from `Action.gd`
2. Implement the required methods (`start()`, `is_completed()`, etc.)
3. Add any specific logic for the action type
4. Use in cutscenes like any other action
This design provides a solid foundation for a flexible cutscene system that can handle both sequential and parallel actions while being easily extensible for new action types.

View File

@@ -1,148 +0,0 @@
# Cutscene System Summary
## Overview
This document provides a comprehensive summary of the cutscene system designed for a 2D point-and-click adventure game in Godot 4.3. The system supports both sequential and parallel action execution, with a focus on extensibility and dynamic behavior.
## System Architecture
### Core Components
1. **CutsceneManager**
- Central orchestrator for all cutscene actions
- Manages sequential and parallel action execution
- Handles action completion and state transitions
- Provides signals for cutscene events
2. **Action (Base Class)**
- Abstract base class for all action types
- Defines common interface and state management
- Implements signal-based completion system
- Supports callbacks for flexible chaining
3. **Specific Action Types**
- MoveAction: Character movement to specific positions
- TurnAction: Character rotation to face targets
- DialogueAction: Text display for conversations
- AnimationAction: Playing character animations
- WaitAction: Time-based delays
### Key Features
1. **Sequential Execution**
- Actions execute one after another in order
- Each action must complete before the next begins
- Supports complex linear story sequences
2. **Parallel Execution**
- Multiple actions can run simultaneously
- System waits for all actions in a group to complete
- Enables synchronized character movements and actions
3. **Dynamic Behavior**
- Actions are not immediate; they take time to complete
- Frame-based updates for smooth animations
- Asynchronous completion through signal system
4. **Extensibility**
- Easy to add new action types by extending the base Action class
- Plugin architecture for complex extensions
- Custom event and callback systems
## Implementation Details
### File Structure
```
/cutscene/
├── CutsceneManager.gd
├── Action.gd (base class)
├── actions/
│ ├── MoveAction.gd
│ ├── TurnAction.gd
│ ├── DialogueAction.gd
│ ├── AnimationAction.gd
│ └── WaitAction.gd
└── examples/
└── sample_cutscene.gd
```
### Example Usage
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 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()
```
## Technical Design
### State Management
- Actions have three states: PENDING, RUNNING, COMPLETED
- CutsceneManager tracks overall state: IDLE, RUNNING, PAUSED
- Proper state transitions with signal notifications
### Completion System
- Signal-based completion notifications
- Parallel action group tracking
- Error handling and recovery mechanisms
- Callback support for flexible action chaining
### Performance Considerations
- Object pooling support for frequently created actions
- Efficient signal connection management
- Minimal overhead for inactive actions
- Frame-based updates for smooth animations
## Extensibility Features
### Adding New Actions
1. Create a new class extending Action
2. Implement required methods (start, is_completed)
3. Add custom logic in update() if needed
4. Use the action in cutscenes like any other
### System Extensions
- Plugin architecture for complex features
- Custom event system for dynamic triggers
- Integration points with game state management
- Save/load support for persistent cutscenes
## Benefits
1. **Flexibility**: Supports both simple linear sequences and complex parallel actions
2. **Extensibility**: Easy to add new action types and system features
3. **Integration**: Designed to work with existing Godot systems
4. **Performance**: Optimized for smooth gameplay and animations
5. **Maintainability**: Clean separation of concerns and clear interfaces
## Next Steps
To implement this system:
1. Create the base Action class
2. Implement the CutsceneManager
3. Develop the core action types
4. Add the completion and callback system
5. Create sample cutscenes for testing
6. Extend with custom actions as needed
This design provides a solid foundation for a powerful, flexible cutscene system that can handle the requirements of a 2D point-and-click adventure game while remaining extensible for future needs.

View File

@@ -1,300 +0,0 @@
# Cutscene System Extensibility Guide
## Overview
This document explains how to extend the cutscene system with new action types, features, and customizations while maintaining compatibility with the existing architecture.
## Adding New Action Types
### Basic Extension Process
1. Create a new class that extends `Action`
2. Implement the required methods (`start()`, `is_completed()`)
3. Optionally implement `update()` and `stop()`
4. Add any custom properties or methods needed
### Example: Custom FadeAction
```gdscript
class_name FadeAction
extends Action
# Custom properties
var target_node: CanvasItem
var target_alpha: float
var fade_speed: float = 1.0
var start_alpha: float = 1.0
func _init(node: CanvasItem, alpha: float, speed: float = 1.0):
target_node = node
target_alpha = alpha
fade_speed = speed
name = "FadeAction"
func start() -> void:
if target_node == null:
._set_failed("Target node is null")
return
start_alpha = target_node.modulate.a
._set_running()
func update(delta: float) -> void:
if state != State.RUNNING:
return
if target_node == null:
._set_failed("Target node was destroyed")
return
# Calculate new alpha value
var current_alpha = target_node.modulate.a
var alpha_diff = target_alpha - current_alpha
var alpha_change = sign(alpha_diff) * fade_speed * delta
# Check if we've reached the target
if abs(alpha_change) >= abs(alpha_diff):
# Set final alpha and complete
var new_modulate = target_node.modulate
new_modulate.a = target_alpha
target_node.modulate = new_modulate
._set_completed()
else:
# Apply incremental change
var new_modulate = target_node.modulate
new_modulate.a += alpha_change
target_node.modulate = new_modulate
func is_completed() -> bool:
return state == State.COMPLETED
```
## Customizing the CutsceneManager
### Adding New Features
The CutsceneManager can be extended with new functionality:
```gdscript
# Extended CutsceneManager with additional features
class_name ExtendedCutsceneManager
extends CutsceneManager
# Custom properties
var skip_enabled: bool = true
var auto_skip_delay: float = 0.0
# Custom signals
signal cutscene_skipped()
# Custom methods
func skip_cutscene() -> void:
if not skip_enabled:
return
emit_signal("cutscene_skipped")
stop()
func set_auto_skip(seconds: float) -> void:
auto_skip_delay = seconds
# Implementation for auto-skip functionality
```
### Plugin Architecture
For complex extensions, consider a plugin-style architecture:
```gdscript
# Plugin interface
class_name CutscenePlugin
extends RefCounted
func initialize(manager: CutsceneManager) -> void:
# Initialize plugin with the manager
pass
func process_action(action: Action, delta: float) -> void:
# Process action each frame
pass
func cleanup() -> void:
# Clean up when cutscene ends
pass
# Example plugin for debugging
class_name DebugCutscenePlugin
extends CutscenePlugin
func process_action(action: Action, delta: float) -> void:
if action.state == Action.State.RUNNING:
print("Action %s is running" % action.name)
```
## Integration with External Systems
### Game State Integration
Connect the cutscene system to your game's state management:
```gdscript
# Integration with a game state manager
class_name GameStateIntegratedCutsceneManager
extends CutsceneManager
var game_state_manager: GameStateManager
func start() -> void:
# Notify game state manager
if game_state_manager:
game_state_manager.set_state(GameState.CUTSCENE)
# Call parent start method
super().start()
func _on_cutscene_completed() -> void:
# Notify game state manager
if game_state_manager:
game_state_manager.set_state(GameState.PLAYING)
# Call parent completion handler
super()._on_cutscene_completed()
```
### Save/Load System Integration
Add support for saving and loading cutscene states:
```gdscript
class_name SaveableCutsceneManager
extends CutsceneManager
func save_state() -> Dictionary:
return {
"current_action_index": action_index,
"sequential_actions": _serialize_actions(sequential_actions),
"parallel_groups": _serialize_parallel_groups(active_parallel_groups),
"state": state
}
func load_state(data: Dictionary) -> void:
action_index = data["current_action_index"]
sequential_actions = _deserialize_actions(data["sequential_actions"])
active_parallel_groups = _deserialize_parallel_groups(data["parallel_groups"])
state = data["state"]
```
## Performance Optimization Extensions
### Action Pooling
Implement object pooling for frequently created actions:
```gdscript
class_name PooledCutsceneManager
extends CutsceneManager
var action_pools: Dictionary = {}
func get_pooled_action(action_type: String, params: Array) -> Action:
if not action_pools.has(action_type):
action_pools[action_type] = []
var pool = action_pools[action_type]
if pool.size() > 0:
# Reuse existing action
var action = pool.pop_back()
# Reinitialize with new parameters
action.reinitialize(params)
return action
else:
# Create new action
return _create_action(action_type, params)
func return_action_to_pool(action: Action) -> void:
var action_type = action.get_class()
if not action_pools.has(action_type):
action_pools[action_type] = []
action.reset()
action_pools[action_type].append(action)
```
## Custom Action Composition
### Action Groups
Create reusable action groups for common sequences:
```gdscript
class_name ActionGroup
extends Action
var actions: Array
var current_action_index: int = 0
func _init(group_actions: Array):
actions = group_actions
name = "ActionGroup"
func start() -> void:
current_action_index = 0
if actions.size() > 0:
_execute_action(actions[0])
else:
._set_completed()
func _execute_action(action: Action) -> void:
action.connect("completed", _on_sub_action_completed.bind(action))
action.start()
func _on_sub_action_completed(action: Action) -> void:
current_action_index += 1
if current_action_index < actions.size():
_execute_action(actions[current_action_index])
else:
._set_completed()
```
## Event-Driven Extensions
### Custom Events
Add support for custom events that can trigger actions:
```gdscript
class_name EventDrivenCutsceneManager
extends CutsceneManager
var event_listeners: Dictionary = {}
func listen_for_event(event_name: String, action: Action) -> void:
if not event_listeners.has(event_name):
event_listeners[event_name] = []
event_listeners[event_name].append(action)
func trigger_event(event_name: String) -> void:
if event_listeners.has(event_name):
for action in event_listeners[event_name]:
# Add action to current sequence or execute immediately
add_action(action)
```
## Best Practices for Extensions
### 1. Maintain Compatibility
- Always call parent methods when overriding
- Keep the same method signatures
- Don't change the core behavior of existing methods
### 2. Use Composition Over Inheritance
- Prefer adding functionality through plugins or components
- Keep inheritance hierarchies shallow
- Use interfaces where possible
### 3. Provide Clear Extension Points
- Document which methods are safe to override
- Provide virtual methods for customization
- Use signals for loose coupling
### 4. Handle Errors Gracefully
- Always check for null references
- Provide meaningful error messages
- Implement fallback behaviors
### 5. Optimize for Performance
- Reuse objects when possible
- Avoid unnecessary processing
- Profile extensions for performance impact
This extensibility guide provides a framework for expanding the cutscene system while maintaining its core functionality and ease of use.

View File

@@ -1,229 +0,0 @@
# Cutscene System Implementation Roadmap
## Overview
This document outlines a step-by-step roadmap for implementing the cutscene system in Godot 4.3, based on the architectural designs created.
## Phase 1: Foundation
### 1.1 Base Action Class
- Create `Action.gd` base class
- Implement state management (PENDING, RUNNING, COMPLETED)
- Add core signals (started, completed, failed)
- Implement basic methods (start, update, is_completed, stop)
### 1.2 CutsceneManager Core
- Create `CutsceneManager.gd`
- Implement sequential action queue
- Add action addition methods
- Create basic state management (IDLE, RUNNING)
## Phase 2: Core Action Types
### 2.1 WaitAction
- Simplest action to implement and test
- Validates the basic action system
- Tests completion signaling
### 2.2 MoveAction
- Implement character movement logic
- Add position calculation and frame-based updates
- Test with different speeds and distances
### 2.3 TurnAction
- Implement character rotation logic
- Handle both position and node targets
- Test smooth rotation animations
## Phase 3: Advanced Features
### 3.1 Parallel Execution System
- Implement parallel action group management
- Add completion tracking for parallel actions
- Test with multiple simultaneous actions
### 3.2 DialogueAction
- Implement basic dialogue display
- Add duration-based completion
- Test with different text lengths
### 3.3 AnimationAction
- Implement animation playback
- Handle AnimationPlayer integration
- Test with looping and non-looping animations
## Phase 4: Completion and Callback System
### 4.1 Signal Integration
- Connect action signals to CutsceneManager
- Implement action completion handling
- Add error handling and failure recovery
### 4.2 Callback System
- Implement action-level callbacks
- Add cutscene-level callbacks
- Test callback chaining
### 4.3 State Management
- Implement full state transitions
- Add pause/resume functionality
- Test state persistence
## Phase 5: Testing and Examples
### 5.1 Basic Cutscene Example
- Create simple sequential cutscene
- Test all core action types
- Validate completion flow
### 5.2 Complex Cutscene Example
- Implement the requirement example:
- Character1 moves to 234, 591
- Character2 moves to 912, 235 (simultaneously)
- Character2 turns to Character1
- Character2 says dialogue
- Character1 plays shocked animation
- Test parallel execution
- Validate timing and synchronization
### 5.3 Edge Case Testing
- Test empty action queues
- Test failed actions
- Test rapid state changes
- Test memory management
## Phase 6: Extensibility Features
### 6.1 Custom Action Example
- Create a custom action type
- Demonstrate extension process
- Validate compatibility with core system
### 6.2 Plugin Architecture
- Implement basic plugin system
- Create example plugin
- Test plugin integration
### 6.3 Performance Optimization
- Implement object pooling
- Add performance monitoring
- Optimize update loops
## Implementation Order
```mermaid
graph TD
A[Phase 1: Foundation] --> B[Phase 2: Core Actions]
B --> C[Phase 3: Advanced Features]
C --> D[Phase 4: Completion System]
D --> E[Phase 5: Testing]
E --> F[Phase 6: Extensibility]
A1[Action Base Class] --> A2[CutsceneManager Core]
A --> A1
A --> A2
B1[WaitAction] --> B2[MoveAction]
B2 --> B3[TurnAction]
B --> B1
B --> B2
B --> B3
C1[Parallel Execution] --> C2[DialogueAction]
C2 --> C3[AnimationAction]
C --> C1
C --> C2
C --> C3
D1[Signal Integration] --> D2[Callback System]
D2 --> D3[State Management]
D --> D1
D --> D2
D --> D3
E1[Basic Example] --> E2[Complex Example]
E2 --> E3[Edge Case Testing]
E --> E1
E --> E2
E --> E3
F1[Custom Action] --> F2[Plugin Architecture]
F2 --> F3[Performance Optimization]
F --> F1
F --> F2
F --> F3
```
## Testing Strategy
### Unit Tests
- Each action type tested independently
- CutsceneManager state transitions
- Signal emission and connection
- Error handling paths
### Integration Tests
- Sequential action execution
- Parallel action execution
- Mixed sequential/parallel sequences
- Callback chaining
### Performance Tests
- Large action queues
- Multiple simultaneous cutscenes
- Memory allocation/garbage collection
- Frame rate impact
## File Structure Implementation
```
res://
└── cutscene/
├── CutsceneManager.gd
├── Action.gd
├── actions/
│ ├── MoveAction.gd
│ ├── TurnAction.gd
│ ├── DialogueAction.gd
│ ├── AnimationAction.gd
│ └── WaitAction.gd
├── examples/
│ ├── basic_cutscene.tscn
│ ├── complex_cutscene.tscn
│ └── custom_action_example.gd
└── tests/
├── test_action.gd
├── test_cutscene_manager.gd
└── test_parallel_execution.gd
```
## Success Criteria
### Functional Requirements
- [ ] Sequential actions execute in order
- [ ] Parallel actions execute simultaneously
- [ ] All core action types function correctly
- [ ] Completion system works reliably
- [ ] Error handling is robust
### Performance Requirements
- [ ] Frame rate remains stable during cutscenes
- [ ] Memory usage is optimized
- [ ] No significant garbage collection spikes
### Extensibility Requirements
- [ ] New action types can be added easily
- [ ] System can be extended with plugins
- [ ] Custom callbacks work as expected
## Timeline Estimate
### Phase 1: Foundation - 2 days
### Phase 2: Core Action Types - 3 days
### Phase 3: Advanced Features - 2 days
### Phase 4: Completion and Callback System - 2 days
### Phase 5: Testing and Examples - 3 days
### Phase 6: Extensibility Features - 2 days
**Total Estimated Time: 14 days**
This roadmap provides a structured approach to implementing the cutscene system, ensuring that each component is properly tested and integrated before moving on to the next phase.

View File

@@ -14,3 +14,7 @@ config/name="adventure-ai"
run/main_scene="res://cutscene/examples/sample_cutscene.tscn"
config/features=PackedStringArray("4.3", "Forward Plus")
config/icon="res://icon.svg"
[editor_plugins]
enabled=PackedStringArray("res://addons/cutscene_editor/plugin.cfg")

View File

@@ -1,242 +0,0 @@
# Sample Cutscene Example
## Overview
This document provides a complete example of how to use the cutscene system to create a complex sequence as described in the requirements.
## Example Scenario
The example implements this sequence:
1. Character1 moves to position (234, 591)
2. At the same time, Character2 moves to position (912, 235)
3. When both arrive, Character2 turns to face Character1
4. Character2 says a dialogue line to Character1
5. A special animation (shocked) is played for Character1
## Sample Implementation
```gdscript
# Sample cutscene implementation
extends Node2D
# Character nodes (would be set in the editor or loaded)
var character1: Node2D
var character2: Node2D
# Cutscene manager
var cutscene_manager: CutsceneManager
func _ready() -> void:
# Initialize the cutscene system
setup_cutscene()
# Start the cutscene
cutscene_manager.start()
func setup_cutscene() -> void:
# Create the cutscene manager
cutscene_manager = CutsceneManager.new()
add_child(cutscene_manager)
# Create the action sequence as described in requirements
# 1. & 2. Characters move simultaneously
var parallel_moves = [
MoveAction.new(character1, Vector2(234, 591), 150.0), # Character1 moves
MoveAction.new(character2, Vector2(912, 235), 150.0) # Character2 moves
]
cutscene_manager.add_parallel_actions(parallel_moves)
# 3. Character2 turns to face Character1
var turn_action = TurnAction.new(character2, character1, 3.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
var animation_action = AnimationAction.new(character1, "shocked", false)
cutscene_manager.add_action(animation_action)
# Set up cutscene callbacks
cutscene_manager.on_completed = func():
print("Cutscene completed!")
# Return to gameplay or next scene
# Alternative implementation with more complex chaining
func setup_advanced_cutscene() -> void:
cutscene_manager = CutsceneManager.new()
add_child(cutscene_manager)
# Create actions
var move1 = MoveAction.new(character1, Vector2(234, 591), 150.0)
var move2 = MoveAction.new(character2, Vector2(912, 235), 150.0)
var turn = TurnAction.new(character2, character1, 3.0)
var dialogue = DialogueAction.new(character2, "Hello there, friend!", 2.0)
var animation = AnimationAction.new(character1, "shocked", false)
# Add parallel actions
cutscene_manager.add_parallel_actions([move1, move2])
# Add sequential actions
cutscene_manager.add_action(turn)
cutscene_manager.add_action(dialogue)
cutscene_manager.add_action(animation)
# Set completion callback
cutscene_manager.on_completed = _on_cutscene_finished
}
func _on_cutscene_finished() -> void:
print("The cutscene has finished playing")
# Handle post-cutscene logic here
# For example, transition to gameplay state
```
## More Complex Example with Conditional Logic
```gdscript
# Advanced example with conditional actions
func setup_conditional_cutscene() -> void:
cutscene_manager = CutsceneManager.new()
add_child(cutscene_manager)
# Move characters into position
var move_group = [
MoveAction.new(character1, Vector2(300, 400)),
MoveAction.new(character2, Vector2(500, 400))
]
cutscene_manager.add_parallel_actions(move_group)
# Conditional dialogue based on game state
var dialogue_action = DialogueAction.new(
character1,
get_dialogue_text(),
3.0
)
cutscene_manager.add_action(dialogue_action)
# Play different animation based on dialogue response
var animation_action = AnimationAction.new(
character2,
get_reaction_animation(),
false
)
cutscene_manager.add_action(animation_action)
# Wait for a moment
cutscene_manager.add_action(WaitAction.new(1.0))
# Final action
var final_dialogue = DialogueAction.new(
character2,
"That was interesting!",
2.0
)
cutscene_manager.add_action(final_dialogue)
func get_dialogue_text() -> String:
# This could be based on game state, player choices, etc.
return "What do you think about this situation?"
func get_reaction_animation() -> String:
# This could be based on game state, player choices, etc.
return "thoughtful"
```
## Integration with Game Systems
```gdscript
# Example of integrating with a game state manager
class_name GameCutsceneManager
extends Node
var cutscene_manager: CutsceneManager
var game_state: GameStateManager
func play_story_cutscene(story_key: String) -> void:
# Pause gameplay
game_state.set_state(GameState.CUTSCENE)
# Create cutscene based on story key
var cutscene_data = load_cutscene_data(story_key)
cutscene_manager = create_cutscene_from_data(cutscene_data)
# Set up completion callback to resume gameplay
cutscene_manager.on_completed = func():
game_state.set_state(GameState.PLAYING)
# Clean up
cutscene_manager.queue_free()
cutscene_manager = null
# Start the cutscene
cutscene_manager.start()
func load_cutscene_data(key: String) -> Dictionary:
# Load cutscene data from a file or database
# This could be JSON, CSV, or custom format
return {
"actions": [
{"type": "move", "character": "player", "position": [100, 200]},
{"type": "dialogue", "character": "npc", "text": "Hello!"},
{"type": "animation", "character": "player", "name": "wave"}
]
}
func create_cutscene_from_data(data: Dictionary) -> CutsceneManager:
var manager = CutsceneManager.new()
add_child(manager)
# Convert data to actions
for action_data in data["actions"]:
var action = create_action_from_data(action_data)
if action:
manager.add_action(action)
return manager
func create_action_from_data(data: Dictionary) -> Action:
match data["type"]:
"move":
var character = get_character_by_name(data["character"])
var position = Vector2(data["position"][0], data["position"][1])
return MoveAction.new(character, position)
"dialogue":
var character = get_character_by_name(data["character"])
return DialogueAction.new(character, data["text"])
"animation":
var character = get_character_by_name(data["character"])
return AnimationAction.new(character, data["name"])
_:
return null
```
## Performance Considerations
```gdscript
# Example of optimizing cutscene performance
func setup_optimized_cutscene() -> void:
cutscene_manager = CutsceneManager.new()
add_child(cutscene_manager)
# Pre-load any resources needed for actions
preload_animations()
preload_dialogue_textures()
# Use object pooling for frequently created actions
var move_action_pool = []
for i in range(10):
move_action_pool.append(MoveAction.new(null, Vector2.ZERO))
# Reuse actions when possible
var action1 = get_pooled_move_action(character1, Vector2(100, 100))
var action2 = get_pooled_move_action(character2, Vector2(200, 200))
cutscene_manager.add_parallel_actions([action1, action2])
# Clean up when done
cutscene_manager.on_completed = func():
release_pooled_actions()
# Other cleanup
```
This sample demonstrates how to use the cutscene system to create complex, dynamic sequences that can handle both sequential and parallel actions while maintaining clean, readable code.