progress.
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
215
addons/cutscene_editor/CutsceneEditorPlugin.gd
Normal 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()
|
||||
59
addons/cutscene_editor/README.md
Normal 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)
|
||||
312
addons/cutscene_editor/editor/CutsceneGenerator.gd
Normal 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, " ")
|
||||
634
addons/cutscene_editor/editor/CutsceneGraphEdit.gd
Normal 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()
|
||||
274
addons/cutscene_editor/editor/PreviewManager.gd
Normal 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
|
||||
186
addons/cutscene_editor/editor/PreviewPanel.gd
Normal 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
|
||||
246
addons/cutscene_editor/editor/UndoRedoManager.gd
Normal 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 []
|
||||
62
addons/cutscene_editor/editor/nodes/AnimationActionNode.gd
Normal 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)
|
||||
284
addons/cutscene_editor/editor/nodes/BaseGraphNode.gd
Normal 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()
|
||||
68
addons/cutscene_editor/editor/nodes/DialogueActionNode.gd
Normal 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)
|
||||
20
addons/cutscene_editor/editor/nodes/EntryNode.gd
Normal 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
|
||||
20
addons/cutscene_editor/editor/nodes/ExitNode.gd
Normal 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
|
||||
88
addons/cutscene_editor/editor/nodes/MoveActionNode.gd
Normal 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)
|
||||
126
addons/cutscene_editor/editor/nodes/ParallelGroupNode.gd
Normal 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"
|
||||
63
addons/cutscene_editor/editor/nodes/TurnActionNode.gd
Normal 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)
|
||||
35
addons/cutscene_editor/editor/nodes/WaitActionNode.gd
Normal 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)
|
||||
121
addons/cutscene_editor/editor/resources/CutsceneResource.gd
Normal 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()
|
||||
83
addons/cutscene_editor/examples/example_cutscene.gd
Normal 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()
|
||||
18
addons/cutscene_editor/examples/example_cutscene.tscn
Normal 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)
|
||||
6
addons/cutscene_editor/icons/icon_animation.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_animation.svg.import
Normal 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
|
||||
5
addons/cutscene_editor/icons/icon_dialogue.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_dialogue.svg.import
Normal 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
|
||||
4
addons/cutscene_editor/icons/icon_entry.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_entry.svg.import
Normal 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
|
||||
4
addons/cutscene_editor/icons/icon_exit.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_exit.svg.import
Normal 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
|
||||
6
addons/cutscene_editor/icons/icon_move.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_move.svg.import
Normal 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
|
||||
6
addons/cutscene_editor/icons/icon_parallel.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_parallel.svg.import
Normal 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
|
||||
5
addons/cutscene_editor/icons/icon_turn.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_turn.svg.import
Normal 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
|
||||
6
addons/cutscene_editor/icons/icon_wait.svg
Normal 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 |
37
addons/cutscene_editor/icons/icon_wait.svg.import
Normal 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
|
||||
7
addons/cutscene_editor/plugin.cfg
Normal 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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||