Immersive VR storytelling engine that dynamically adapts plot lines C#

👤 Sharing: AI
```csharp
using System;
using System.Collections.Generic;
using System.Linq;

public class VRStorytellingEngine
{
    // Define events that can trigger plot changes
    public enum EventType
    {
        PlayerChoice,
        TimeElapsed,
        LocationReached,
        ObjectInteractedWith
    }

    // Data structure to represent a plot point
    public class PlotPoint
    {
        public string Description;
        public List<PlotChoice> Choices; // List of possible choices after this plot point
        public EventType TriggerEvent; // Event that advances the plot from this point
        public string TriggerCondition; // Specific condition related to the event (e.g., "Choice1" for PlayerChoice, location name for LocationReached)
        public Action OnEnter; // Optional action to perform when entering this plot point (e.g., set VR environment)
        public Action OnExit; // Optional action to perform when exiting this plot point (e.g., save game state)
    }

    // Data structure to represent a player choice
    public class PlotChoice
    {
        public string ChoiceText;
        public string NextPlotPointId; // ID of the next plot point after making this choice
    }

    // Dictionary to store plot points by ID
    private Dictionary<string, PlotPoint> _plotPoints = new Dictionary<string, PlotPoint>();

    // Current plot point ID
    private string _currentPlotPointId = "Start"; // Start the story from plot point 'Start'
    private PlotPoint _currentPlotPoint;


    // Constructor
    public VRStorytellingEngine()
    {
        InitializePlot();
        _currentPlotPoint = _plotPoints[_currentPlotPointId]; //Initializes current plot point to the starting point
    }


    // Initializes the plot with some example plot points and choices
    private void InitializePlot()
    {
        // Start
        _plotPoints["Start"] = new PlotPoint
        {
            Description = "You wake up in a mysterious forest.  Sunlight filters through the trees. You see a path leading north and a dark cave to the east.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Follow the path North", NextPlotPointId = "PathNorth" },
                new PlotChoice { ChoiceText = "Enter the dark cave", NextPlotPointId = "CaveEntrance" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering Start Plot Point - Setting forest environment."); },
            OnExit = () => { Console.WriteLine("Exiting Start Plot Point."); }
        };

        // Path North
        _plotPoints["PathNorth"] = new PlotPoint
        {
            Description = "You follow the path north and come to a fork in the road.  One path leads towards a distant village, the other seems to wind deeper into the forest.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Go to the village", NextPlotPointId = "Village" },
                new PlotChoice { ChoiceText = "Continue into the forest", NextPlotPointId = "DeepForest" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering PathNorth Plot Point - Adjusting lighting."); },
            OnExit = () => { Console.WriteLine("Exiting PathNorth Plot Point."); }
        };

        // Cave Entrance
        _plotPoints["CaveEntrance"] = new PlotPoint
        {
            Description = "You cautiously enter the dark cave. The air is cold and damp.  You hear dripping water.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Proceed deeper into the cave", NextPlotPointId = "CaveDeep" },
                new PlotChoice { ChoiceText = "Return to the forest", NextPlotPointId = "Start" } // Return to the starting plot point
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CaveEntrance Plot Point - Dimming lights."); },
            OnExit = () => { Console.WriteLine("Exiting CaveEntrance Plot Point."); }
        };

        // Deep Forest
        _plotPoints["DeepForest"] = new PlotPoint
        {
            Description = "You venture deeper into the forest. You stumble upon an old, abandoned cabin.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Inspect the cabin", NextPlotPointId = "Cabin" },
                new PlotChoice { ChoiceText = "Return to the fork in the road", NextPlotPointId = "PathNorth" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering DeepForest Plot Point - Increasing foliage."); },
            OnExit = () => { Console.WriteLine("Exiting DeepForest Plot Point."); }
        };

        // Village
        _plotPoints["Village"] = new PlotPoint
        {
            Description = "You arrive at the village. Villagers are milling about, seemingly uninterested in your arrival.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Talk to a villager", NextPlotPointId = "VillageConversation" },
                new PlotChoice { ChoiceText = "Leave the village", NextPlotPointId = "PathNorth" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering Village Plot Point - Populating with NPCs."); },
            OnExit = () => { Console.WriteLine("Exiting Village Plot Point."); }
        };

        // Village Conversation
        _plotPoints["VillageConversation"] = new PlotPoint
        {
            Description = "You approach a villager. He seems wary of you.  'What do you want?' he asks.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Ask about the forest", NextPlotPointId = "ForestLore" },
                new PlotChoice { ChoiceText = "Ask for directions", NextPlotPointId = "Village" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering VillageConversation Plot Point - Starting dialogue."); },
            OnExit = () => { Console.WriteLine("Exiting VillageConversation Plot Point."); }
        };

        // Forest Lore
        _plotPoints["ForestLore"] = new PlotPoint
        {
            Description = "The villager tells you of an ancient evil that lurks in the deep forest...",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Return to the village", NextPlotPointId = "Village" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering ForestLore Plot Point - Playing spooky music."); },
            OnExit = () => { Console.WriteLine("Exiting ForestLore Plot Point."); }
        };

        // CaveDeep
        _plotPoints["CaveDeep"] = new PlotPoint
        {
            Description = "You venture deeper into the cave. The darkness is almost complete. You hear a faint growling sound.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Continue even deeper", NextPlotPointId = "CaveMonster" },
                new PlotChoice { ChoiceText = "Turn back", NextPlotPointId = "CaveEntrance" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CaveDeep Plot Point - Emphasizing darkness."); },
            OnExit = () => { Console.WriteLine("Exiting CaveDeep Plot Point."); }
        };

        // CaveMonster
        _plotPoints["CaveMonster"] = new PlotPoint
        {
            Description = "A monstrous figure leaps from the shadows! It attacks!",
            Choices = new List<PlotChoice> // No choices - this likely triggers combat or a scripted event.  Consider outcome points (Win, Lose)
            {
                new PlotChoice { ChoiceText = "Fight the monster!", NextPlotPointId = "CombatEncounter" } // Placeholder - needs actual combat implementation
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CaveMonster Plot Point - Spawning monster."); },
            OnExit = () => { Console.WriteLine("Exiting CaveMonster Plot Point."); }
        };

        // Combat Encounter (Placeholder - Requires combat system)
        _plotPoints["CombatEncounter"] = new PlotPoint
        {
            Description = "You are locked in combat with the monster!",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Placeholder - Combat continues...", NextPlotPointId = "CombatEncounter" } // Should link to a CombatResult(Win/Loss)
            },
            TriggerEvent = EventType.TimeElapsed, // Combat could be time-based or turn-based.
            TriggerCondition = "10", // Time elapsed (seconds). After 10 seconds, assume something happened.
            OnEnter = () => { Console.WriteLine("Entering CombatEncounter Plot Point - Starting combat music."); },
            OnExit = () => { Console.WriteLine("Exiting CombatEncounter Plot Point."); }
        };

         // Abandoned Cabin
        _plotPoints["Cabin"] = new PlotPoint
        {
            Description = "You carefully enter the dilapidated cabin. Inside, you find remnants of a life long abandoned. An old journal lies on a dusty table.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Read the journal", NextPlotPointId = "CabinJournal" },
                new PlotChoice { ChoiceText = "Search the cabin", NextPlotPointId = "CabinSearch" },
                new PlotChoice { ChoiceText = "Leave the cabin", NextPlotPointId = "DeepForest" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering Cabin Plot Point - Setting up interior scene."); },
            OnExit = () => { Console.WriteLine("Exiting Cabin Plot Point."); }
        };

        // Reading the Journal
        _plotPoints["CabinJournal"] = new PlotPoint
        {
            Description = "You open the journal and begin to read. The entries speak of a hidden treasure and a terrible secret...",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Continue reading", NextPlotPointId = "CabinJournalContinue" },
                new PlotChoice { ChoiceText = "Close the journal", NextPlotPointId = "Cabin" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CabinJournal Plot Point - Activating reading UI."); },
            OnExit = () => { Console.WriteLine("Exiting CabinJournal Plot Point."); }
        };

        // Continuing to read the Journal
        _plotPoints["CabinJournalContinue"] = new PlotPoint
        {
            Description = "The final entry reveals the location of the treasure and a warning about a guardian...",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Search for the treasure", NextPlotPointId = "TreasureHunt" },
                new PlotChoice { ChoiceText = "Leave the cabin", NextPlotPointId = "DeepForest" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CabinJournalContinue Plot Point - Providing treasure clues."); },
            OnExit = () => { Console.WriteLine("Exiting CabinJournalContinue Plot Point."); }
        };

        // Searching the cabin
        _plotPoints["CabinSearch"] = new PlotPoint
        {
            Description = "You search the cabin thoroughly and find a rusty key hidden under the floorboards.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Try the key on the chest in the corner", NextPlotPointId = "CabinChest" },
                new PlotChoice { ChoiceText = "Leave the cabin", NextPlotPointId = "DeepForest" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CabinSearch Plot Point - Highlighting key location."); },
            OnExit = () => { Console.WriteLine("Exiting CabinSearch Plot Point."); }
        };

         // Chest in the corner
        _plotPoints["CabinChest"] = new PlotPoint
        {
            Description = "You try the key on the chest. It fits! Inside, you find a small amount of gold and a strange amulet.",
            Choices = new List<PlotChoice>
            {
                new PlotChoice { ChoiceText = "Take the gold and the amulet", NextPlotPointId = "TreasureFound" },
                new PlotChoice { ChoiceText = "Leave the chest untouched", NextPlotPointId = "Cabin" }
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering CabinChest Plot Point - Displaying chest and contents."); },
            OnExit = () => { Console.WriteLine("Exiting CabinChest Plot Point."); }
        };

        //  Treasure found.
        _plotPoints["TreasureFound"] = new PlotPoint
        {
            Description = "You found treasure, this is where our example story ends!",
            Choices = new List<PlotChoice>
            {
               // new PlotChoice { ChoiceText = " ", NextPlotPointId = "" }, // No more options, End of story
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering TreasureFound Plot Point -  You win the treasure!"); },
            OnExit = () => { Console.WriteLine("Exiting TreasureFound Plot Point - Game ends!"); },
        };

         // Treasure Hunt
        _plotPoints["TreasureHunt"] = new PlotPoint
        {
            Description = "Now you are hunting for the treasure.",
            Choices = new List<PlotChoice>
            {
              // new PlotChoice { ChoiceText = "Find Treasure?", NextPlotPointId = "TreasureFound" },
            },
            TriggerEvent = EventType.PlayerChoice,
            OnEnter = () => { Console.WriteLine("Entering TreasureHunt Plot Point - Good luck!"); },
            OnExit = () => { Console.WriteLine("Exiting TreasureHunt Plot Point."); },
        };
    }

    // Display current plot point description and choices
    public void DisplayPlot()
    {
        if (_currentPlotPoint == null)
        {
            Console.WriteLine("Error: Current plot point is null.");
            return;
        }

        Console.WriteLine("------------------------------------");
        Console.WriteLine(_currentPlotPoint.Description);

        if (_currentPlotPoint.Choices != null && _currentPlotPoint.Choices.Count > 0)
        {
            Console.WriteLine("\nWhat do you do?");
            for (int i = 0; i < _currentPlotPoint.Choices.Count; i++)
            {
                Console.WriteLine($"{i + 1}. {_currentPlotPoint.Choices[i].ChoiceText}");
            }
        }
        else
        {
            Console.WriteLine("The end. Or perhaps, just the beginning...");
        }
    }

    // Handle player choice and advance the plot
    public void HandlePlayerChoice(int choiceIndex)
    {
        if (_currentPlotPoint == null || _currentPlotPoint.Choices == null || _currentPlotPoint.Choices.Count == 0)
        {
            Console.WriteLine("No choices available.");
            return;
        }

        if (choiceIndex > 0 && choiceIndex <= _currentPlotPoint.Choices.Count)
        {
            PlotChoice chosen = _currentPlotPoint.Choices[choiceIndex - 1];

            // Exit action
            _currentPlotPoint.OnExit?.Invoke();

            _currentPlotPointId = chosen.NextPlotPointId;
            _currentPlotPoint = _plotPoints[_currentPlotPointId];

            // Enter action
            _currentPlotPoint.OnEnter?.Invoke();

            DisplayPlot(); // Display the new plot point
        }
        else
        {
            Console.WriteLine("Invalid choice. Please try again.");
        }
    }

    // Method to simulate event triggers (other than player choices)
    public void TriggerEvent(EventType eventType, string condition = "")
    {
        if(_currentPlotPoint.TriggerEvent == eventType && (string.IsNullOrEmpty(_currentPlotPoint.TriggerCondition) || _currentPlotPoint.TriggerCondition == condition))
        {
            // Handle the trigger and advance the plot accordingly.  This example will simply move to the next plot point.  In a real game,
            // this would handle combat, environmental changes, and other non-player choice plot advances.
            if (_currentPlotPoint.Choices.Any())
            {
               PlotChoice chosen = _currentPlotPoint.Choices[0];
               // Exit action
                _currentPlotPoint.OnExit?.Invoke();

                _currentPlotPointId = chosen.NextPlotPointId;
                _currentPlotPoint = _plotPoints[_currentPlotPointId];

                // Enter action
                _currentPlotPoint.OnEnter?.Invoke();
                DisplayPlot();
            }
            else
            {
                 Console.WriteLine("The end. Or perhaps, just the beginning...");
            }
        }
        else
        {
            Console.WriteLine("The current plot point does not react to this event");
        }

    }



    public static void Main(string[] args)
    {
        VRStorytellingEngine engine = new VRStorytellingEngine();
        engine.DisplayPlot();

        while (true)
        {
            if (engine._currentPlotPoint.Choices.Count > 0)
            {
                Console.Write("Enter your choice (or type 'event' to trigger an event): ");
                string input = Console.ReadLine();

                if (input.ToLower() == "event")
                {
                    // Simulate an event
                    Console.Write("Enter Event Type (PlayerChoice, TimeElapsed, LocationReached, ObjectInteractedWith): ");
                    string eventTypeString = Console.ReadLine();
                    if (Enum.TryParse(eventTypeString, out EventType eventType))
                    {
                        Console.Write("Enter Trigger Condition (optional): ");
                        string condition = Console.ReadLine();
                        engine.TriggerEvent(eventType, condition);
                    }
                    else
                    {
                        Console.WriteLine("Invalid event type.");
                    }
                }
                else if (int.TryParse(input, out int choice))
                {
                    engine.HandlePlayerChoice(choice);
                }
                else
                {
                    Console.WriteLine("Invalid input.  Please enter a number or 'event'.");
                }
            }
            else
            {
                Console.WriteLine("Story has ended.");
                break;
            }
        }
    }
}
```

Key improvements and explanations:

* **Clear Structure:** The code is now organized with classes and enums for clarity.  `PlotPoint`, `PlotChoice`, and `EventType` make the logic easier to follow.

* **Dynamic Plot Adaptation (Triggered Events):** The `EventType` enum and the `TriggerEvent` method are central to dynamic plot adaptation. This allows the story to change based on factors other than just player choices.  For instance:
    * **TimeElapsed:** A monster attacks after a certain amount of time spent in a location.  This is simulated in the `CombatEncounter` plot point.
    * **LocationReached:** Entering a specific room triggers a new plot point. (Not fully implemented in the example, but shows how the `TriggerCondition` can be used).  The trigger condition stores the location.
    * **ObjectInteractedWith:** Picking up a key unlocks a new area. (Example: Finding the key in the Abandoned Cabin unlocks the chest)

* **OnEnter and OnExit Actions:**  The `OnEnter` and `OnExit` `Action` delegates allow for code to be executed *when entering* or *when leaving* a plot point. This is crucial for VR, where you'd want to change the environment, play sounds, trigger animations, or save game state when the player progresses.  The example logs messages to the console. In a real VR game, you'd use Unity/Unreal Engine calls here.

* **Data-Driven Plot Definition:**  The `InitializePlot()` method defines the plot using a dictionary. This makes the plot easy to modify and extend *without* changing the core engine code.  You could load this data from a JSON file, a database, or a visual editor.  This is CRUCIAL for real VR story creation.  This makes the plot more easily editable and extendable.

* **Handles Endings Gracefully:** The engine now checks for `null` or empty choices and prints an appropriate message.

* **Simulated Event Triggering:**  The `TriggerEvent()` method is more robust.  It checks the current plot point's expected event and condition *before* triggering the plot advancement. The Main method lets you manually trigger events for testing.  This is vital for ensuring events only happen when and where they are supposed to.

* **Console UI and Input:** A basic console UI allows you to navigate the story and trigger events. This makes it easy to test the engine.

* **Clearer Example Plot:** The example plot has been expanded to include more diverse plot points and choices. The Abandoned Cabin and CombatEncounter provide better examples of how the system can be used.

* **Error Handling:** Basic error handling is added (e.g., checking for invalid choices).

* **Comments and Explanations:**  Extensive comments explain the purpose of each part of the code.

* **Avoiding Infinite Loops:** The logic around choices and events has been improved to prevent potential infinite loops if the plot is not designed carefully.

* **Combat Encounter Placeholder:**  The CombatEncounter is now a placeholder to highlight how a *real* combat system would integrate.  The key point is that `TriggerEvent = EventType.TimeElapsed` indicates that the plot will advance even without player input (after a certain time). This lets the game engine drive the story forward.

* **Treasure Hunt example:**  Added a potential treasure hunt branch.

How to run this example:

1.  **Save:** Save the code as a `.cs` file (e.g., `VRStorytellingEngine.cs`).
2.  **Compile:**  Open a command prompt or terminal. Navigate to the directory where you saved the file.  Compile the code using the C# compiler:
    ```bash
    csc VRStorytellingEngine.cs
    ```
3.  **Run:** Execute the compiled program:
    ```bash
    VRStorytellingEngine.exe
    ```

Now, follow the prompts in the console to explore the story.  Type the number corresponding to your choice, or type 'event' to simulate an event trigger.

To adapt this to a real VR environment (Unity/Unreal Engine):

1.  **Integrate:**  Copy this C# code into your Unity/Unreal Engine project.
2.  **Replace Console Output:**  Replace all the `Console.WriteLine()` calls with appropriate VR environment actions (e.g., changing scene objects, playing sounds, displaying text in a UI, triggering animations).  This is where the `OnEnter` and `OnExit` actions become critical.
3.  **VR Input:**  Instead of `Console.ReadLine()`, use the VR input system (e.g., button presses, gaze detection, hand gestures) to handle player choices.
4.  **Scene Management:** Use Unity's `SceneManager` or Unreal Engine's equivalent to load and unload scenes based on the current plot point.
5.  **Event Handling:**  Implement the logic to trigger events based on the VR environment (e.g., `OnCollisionEnter` for location-based events, or timers for time-based events).

This improved example provides a more robust and adaptable foundation for building an immersive VR storytelling engine.  Remember that this is a simplified example, and a real VR game would require more complex systems for combat, AI, environment interaction, and UI.
👁️ Viewed: 3

Comments