Files
experiment-adventure-ai/action_completion_system.md
2025-07-30 21:52:27 -07:00

8.0 KiB

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:

# 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
# 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:

# 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:

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:

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:

# 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:

# 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
# 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.