/** * This code is part of the Fungus library (http://fungusgames.com) maintained by Chris Gregan (http://twitter.com/gofungus). * It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE) */ using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Serialization; using System; using System.Text; using System.Linq; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Fungus { /** * Interface for Flowchart components which can be updated when the * scene loads in the editor. This is used to maintain backwards * compatibility with earlier versions of Fungus. */ interface IUpdateable { void UpdateToVersion(int oldVersion, int newVersion); } /** * Visual scripting controller for the Flowchart programming language. * Flowchart objects may be edited visually using the Flowchart editor window. */ [ExecuteInEditMode] public class Flowchart : MonoBehaviour, StringSubstituter.ISubstitutionHandler { /** * The current version of the Flowchart. Used for updating components. */ public const int CURRENT_VERSION = 1; /** * The name of the initial block in a new flowchart. */ public const string DEFAULT_BLOCK_NAME = "New Block"; /** * Variable to track flowchart's version so components can update to new versions. */ [HideInInspector] [SerializeField] protected int version = 0; // Default to 0 to always trigger an update for older versions of Fungus. public int Version { set { version = value; } } /** * Scroll position of Flowchart editor window. */ [HideInInspector] [SerializeField] protected Vector2 scrollPos; public Vector2 ScrollPos { get { return scrollPos; } set { scrollPos = value; } } /** * Scroll position of Flowchart variables window. */ [HideInInspector] [SerializeField] protected Vector2 variablesScrollPos; public Vector2 VariablesScrollPos { get { return variablesScrollPos; } set { variablesScrollPos = value; } } /** * Show the variables pane. */ [HideInInspector] [SerializeField] protected bool variablesExpanded = true; public bool VariablesExpanded { get { return variablesExpanded; } set { variablesExpanded = value; } } /** * Height of command block view in inspector. */ [HideInInspector] [SerializeField] protected float blockViewHeight = 400; public float BlockViewHeight { get { return blockViewHeight; } set { blockViewHeight = value; } } /** * Zoom level of Flowchart editor window */ [HideInInspector] [SerializeField] protected float zoom = 1f; public float Zoom { get { return zoom; } set { zoom = value; } } /** * Scrollable area for Flowchart editor window. */ [HideInInspector] [SerializeField] protected Rect scrollViewRect; public Rect ScrollViewRect { get { return scrollViewRect; } set { scrollViewRect = value; } } /** * Currently selected block in the Flowchart editor. */ [HideInInspector] [FormerlySerializedAs("selectedSequence")] [SerializeField] protected Block selectedBlock; public Block SelectedBlock { get { return selectedBlock; } set { selectedBlock = value; } } /** * Currently selected command in the Flowchart editor. */ [HideInInspector] [SerializeField] protected List selectedCommands = new List(); public List SelectedCommands { get { return selectedCommands; } } /** * The list of variables that can be accessed by the Flowchart. */ [HideInInspector] [SerializeField] protected List variables = new List(); public List Variables { get { return variables; } } [TextArea(3, 5)] [Tooltip("Description text displayed in the Flowchart editor window")] [SerializeField] protected string description = ""; public string Description { get { return description; } } /** * Slow down execution in the editor to make it easier to visualise program flow. */ [Range(0f, 5f)] [Tooltip("Adds a pause after each execution step to make it easier to visualise program flow. Editor only, has no effect in platform builds.")] [SerializeField] protected float stepPause = 0f; public float StepPause { get { return stepPause; } } /** * Use command color when displaying the command list in the inspector. */ [Tooltip("Use command color when displaying the command list in the Fungus Editor window")] [SerializeField] protected bool colorCommands = true; public bool ColorCommands { get { return colorCommands; } } /** * Hides the Flowchart block and command components in the inspector. * Deselect to inspect the block and command components that make up the Flowchart. */ [Tooltip("Hides the Flowchart block and command components in the inspector")] [SerializeField] protected bool hideComponents = true; /** * Saves the selected block and commands when saving the scene. * Helps avoid version control conflicts if you've only changed the active selection. */ [Tooltip("Saves the selected block and commands when saving the scene.")] [SerializeField] protected bool saveSelection = true; public bool SaveSelection { get { return saveSelection; } } /** * Unique identifier for identifying this flowchart in localized string keys. */ [Tooltip("Unique identifier for this flowchart in localized string keys. If no id is specified then the name of the Flowchart object will be used.")] [SerializeField] protected string localizationId = ""; public string LocalizationId { get { return localizationId; } } /** * Display line numbers in the command list in the Block inspector. */ [Tooltip("Display line numbers in the command list in the Block inspector.")] [SerializeField] protected bool showLineNumbers = false; public bool ShowLineNumbers { get { return showLineNumbers; } } /** * List of commands to hide in the Add Command menu. Use this to restrict the set of commands available when editing a Flowchart. */ [Tooltip("List of commands to hide in the Add Command menu. Use this to restrict the set of commands available when editing a Flowchart.")] [SerializeField] protected List hideCommands = new List(); [Tooltip("Lua Environment to be used by default for all Execute Lua commands in this Flowchart")] [SerializeField] protected LuaEnvironment luaEnvironment; public LuaEnvironment _LuaEnvironment { get { return luaEnvironment; } } /** * The ExecuteLua command adds a global Lua variable with this name bound to the flowchart prior to executing. */ [Tooltip("The ExecuteLua command adds a global Lua variable with this name bound to the flowchart prior to executing.")] [SerializeField] protected string luaBindingName = "flowchart"; public string LuaBindingName { get { return luaBindingName; } } /** * Position in the center of all blocks in the flowchart. */ public Vector2 CenterPosition { set; get; } /** * Cached list of flowchart objects in the scene for fast lookup */ public static List cachedFlowcharts = new List(); protected static bool eventSystemPresent; protected StringSubstituter stringSubstituer; /** * Returns the next id to assign to a new flowchart item. * Item ids increase monotically so they are guaranteed to * be unique within a Flowchart. */ public int NextItemId() { int maxId = -1; Block[] blocks = GetComponents(); foreach (Block block in blocks) { maxId = Math.Max(maxId, block.ItemId); } Command[] commands = GetComponents(); foreach (Command command in commands) { maxId = Math.Max(maxId, command.ItemId); } return maxId + 1; } #if UNITY_5_4_OR_NEWER protected virtual void Awake() { UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (A, B) => { LevelWasLoaded(); }; } #else public virtual void OnLevelWasLoaded(int level) { LevelWasLoaded(); } #endif protected virtual void LevelWasLoaded() { // Reset the flag for checking for an event system as there may not be one in the newly loaded scene. eventSystemPresent = false; } protected virtual void Start() { CheckEventSystem(); } // There must be an Event System in the scene for Say and Menu input to work. // This method will automatically instantiate one if none exists. protected virtual void CheckEventSystem() { if (eventSystemPresent) { return; } EventSystem eventSystem = GameObject.FindObjectOfType(); if (eventSystem == null) { // Auto spawn an Event System from the prefab GameObject prefab = Resources.Load("EventSystem"); if (prefab != null) { GameObject go = Instantiate(prefab) as GameObject; go.name = "EventSystem"; } } eventSystemPresent = true; } public virtual void OnEnable() { if (!cachedFlowcharts.Contains(this)) { cachedFlowcharts.Add(this); } CheckItemIds(); CleanupComponents(); UpdateVersion(); } protected virtual void UpdateVersion() { if (version == CURRENT_VERSION) { // No need to update return; } // Tell all components that implement IUpdateable to update to the new version foreach (Component component in GetComponents()) { IUpdateable u = component as IUpdateable; if (u != null) { u.UpdateToVersion(version, CURRENT_VERSION); } } version = CURRENT_VERSION; } public virtual void OnDisable() { cachedFlowcharts.Remove(this); } protected virtual void CheckItemIds() { // Make sure item ids are unique and monotonically increasing. // This should always be the case, but some legacy Flowcharts may have issues. List usedIds = new List(); Block[] blocks = GetComponents(); foreach (Block block in blocks) { if (block.ItemId == -1 || usedIds.Contains(block.ItemId)) { block.ItemId = NextItemId(); } usedIds.Add(block.ItemId); } Command[] commands = GetComponents(); foreach (Command command in commands) { if (command.ItemId == -1 || usedIds.Contains(command.ItemId)) { command.ItemId = NextItemId(); } usedIds.Add(command.ItemId); } } protected virtual void CleanupComponents() { // Delete any unreferenced components which shouldn't exist any more // Unreferenced components don't have any effect on the flowchart behavior, but // they waste memory so should be cleared out periodically. Block[] blocks = GetComponents(); // Remove any null entries in the variables list // It shouldn't happen but it seemed to occur for a user on the forum variables.RemoveAll(item => item == null); foreach (Variable variable in GetComponents()) { if (!variables.Contains(variable)) { DestroyImmediate(variable); } } foreach (Command command in GetComponents()) { bool found = false; foreach (Block block in blocks) { if (block.CommandList.Contains(command)) { found = true; break; } } if (!found) { DestroyImmediate(command); } } foreach (EventHandler eventHandler in GetComponents()) { bool found = false; foreach (Block block in blocks) { if (block._EventHandler == eventHandler) { found = true; break; } } if (!found) { DestroyImmediate(eventHandler); } } } protected virtual Block CreateBlockComponent(GameObject parent) { Block block = parent.AddComponent(); return block; } /** * Create a new block node which you can then add commands to. */ public virtual Block CreateBlock(Vector2 position) { Block b = CreateBlockComponent(gameObject); b._NodeRect = new Rect(position.x, position.y, 0, 0); b.BlockName = GetUniqueBlockKey(b.BlockName, b); b.ItemId = NextItemId(); return b; } /** * Returns the named Block in the flowchart, or null if not found. */ public virtual Block FindBlock(string blockName) { Block [] blocks = GetComponents(); foreach (Block block in blocks) { if (block.BlockName == blockName) { return block; } } return null; } /** * Execute a child block in the Flowchart. * You can use this method in a UI event. e.g. to handle a button click. * Returns true if the Block started execution. */ public virtual void ExecuteBlock(string blockName) { Block block = null; foreach (Block b in GetComponents()) { if (b.BlockName == blockName) { block = b; break; } } if (block == null) { Debug.LogError("Block " + blockName + "does not exist"); return; } if (!ExecuteBlock(block)) { Debug.LogWarning("Block " + blockName + "failed to execute"); } } /** * Execute a child block in the flowchart. * The block must be in an idle state to be executed. * This version provides extra options to control how the block is executed. * Returns true if the Block started execution. */ public virtual bool ExecuteBlock(Block block, int commandIndex = 0, Action onComplete = null) { if (block == null) { Debug.LogError("Block must not be null"); return false; } if (block.gameObject != gameObject) { Debug.LogError("Block must belong to the same gameobject as this Flowchart"); return false; } // Can't restart a running block, have to wait until it's idle again if (block.IsExecuting()) { return false; } // Start executing the Block as a new coroutine StartCoroutine(block.Execute(commandIndex, onComplete)); return true; } /** * Stop all executing Blocks in this Flowchart. */ public virtual void StopAllBlocks() { Block [] blocks = GetComponents(); foreach (Block block in blocks) { if (block.IsExecuting()) { block.Stop(); } } } /** * Sends a message to this Flowchart only. * Any block with a matching MessageReceived event handler will start executing. */ public virtual void SendFungusMessage(string messageName) { MessageReceived[] eventHandlers = GetComponents(); foreach (MessageReceived eventHandler in eventHandlers) { eventHandler.OnSendFungusMessage(messageName); } } /** * Sends a message to all Flowchart objects in the current scene. * Any block with a matching MessageReceived event handler will start executing. */ public static void BroadcastFungusMessage(string messageName) { MessageReceived[] eventHandlers = UnityEngine.Object.FindObjectsOfType(); foreach (MessageReceived eventHandler in eventHandlers) { eventHandler.OnSendFungusMessage(messageName); } } /** * Returns a new variable key that is guaranteed not to clash with any existing variable in the list. */ public virtual string GetUniqueVariableKey(string originalKey, Variable ignoreVariable = null) { int suffix = 0; string baseKey = originalKey; // Only letters and digits allowed char[] arr = baseKey.Where(c => (char.IsLetterOrDigit(c) || c == '_')).ToArray(); baseKey = new string(arr); // No leading digits allowed baseKey = baseKey.TrimStart('0','1','2','3','4','5','6','7','8','9'); // No empty keys allowed if (baseKey.Length == 0) { baseKey = "Var"; } string key = baseKey; while (true) { bool collision = false; foreach(Variable variable in variables) { if (variable == null || variable == ignoreVariable || variable.Key == null) { continue; } if (variable.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)) { collision = true; suffix++; key = baseKey + suffix; } } if (!collision) { return key; } } } /** * Returns a new Block key that is guaranteed not to clash with any existing Block in the Flowchart. */ public virtual string GetUniqueBlockKey(string originalKey, Block ignoreBlock = null) { int suffix = 0; string baseKey = originalKey.Trim(); // No empty keys allowed if (baseKey.Length == 0) { baseKey = "New Block"; } Block[] blocks = GetComponents(); string key = baseKey; while (true) { bool collision = false; foreach(Block block in blocks) { if (block == ignoreBlock || block.BlockName == null) { continue; } if (block.BlockName.Equals(key, StringComparison.CurrentCultureIgnoreCase)) { collision = true; suffix++; key = baseKey + suffix; } } if (!collision) { return key; } } } /** * Returns a new Label key that is guaranteed not to clash with any existing Label in the Block. */ public virtual string GetUniqueLabelKey(string originalKey, Label ignoreLabel) { int suffix = 0; string baseKey = originalKey.Trim(); // No empty keys allowed if (baseKey.Length == 0) { baseKey = "New Label"; } Block block = ignoreLabel.ParentBlock; string key = baseKey; while (true) { bool collision = false; foreach(Command command in block.CommandList) { Label label = command as Label; if (label == null || label == ignoreLabel) { continue; } if (label.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)) { collision = true; suffix++; key = baseKey + suffix; } } if (!collision) { return key; } } } /** * Returns the variable with the specified key, or null if the key is not found. * You will need to cast the returned variable to the correct sub-type. * You can then access the variable's value using the Value property. e.g. * BooleanVariable boolVar = flowchart.GetVariable("MyBool") as BooleanVariable; * boolVar.Value = false; */ public Variable GetVariable(string key) { foreach (Variable variable in variables) { if (variable != null && variable.Key == key) { return variable; } } return null; } /** * Returns the variable with the specified key, or null if the key is not found. * You can then access the variable's value using the Value property. e.g. * BooleanVariable boolVar = flowchart.GetVariable("MyBool"); * boolVar.Value = false; */ public T GetVariable(string key) where T : Variable { foreach (Variable variable in variables) { if (variable != null && variable.Key == key) { return variable as T; } } Debug.LogWarning("Variable " + key + " not found."); return null; } /** * Register a new variable with the Flowchart at runtime. * The variable should be added as a component on the Flowchart game object. */ public void SetVariable(string key, T newvariable) where T : Variable { foreach (Variable v in variables) { if (v != null && v.Key == key) { T variable = v as T; if (variable != null) { variable = newvariable; return; } } } Debug.LogWarning("Variable " + key + " not found."); } /** * Gets a list of all variables with public scope in this Flowchart. */ public virtual List GetPublicVariables() { List publicVariables = new List(); foreach (Variable v in variables) { if (v != null && v.Scope == VariableScope.Public) { publicVariables.Add(v); } } return publicVariables; } /** * Gets the value of a boolean variable. * Returns false if the variable key does not exist. */ public virtual bool GetBooleanVariable(string key) { BooleanVariable variable = GetVariable(key); if(variable != null) { return GetVariable(key).value; } else { return false; } } /** * Sets the value of a boolean variable. * The variable must already be added to the list of variables for this Flowchart. */ public virtual void SetBooleanVariable(string key, bool value) { BooleanVariable variable = GetVariable(key); if(variable != null) { variable.value = value; } } /** * Gets the value of an integer variable. * Returns 0 if the variable key does not exist. */ public virtual int GetIntegerVariable(string key) { IntegerVariable variable = GetVariable(key); if (variable != null) { return GetVariable(key).value; } else { return 0; } } /** * Sets the value of an integer variable. * The variable must already be added to the list of variables for this Flowchart. */ public virtual void SetIntegerVariable(string key, int value) { IntegerVariable variable = GetVariable(key); if (variable != null) { variable.value = value; } } /** * Gets the value of a float variable. * Returns 0 if the variable key does not exist. */ public virtual float GetFloatVariable(string key) { FloatVariable variable = GetVariable(key); if (variable != null) { return GetVariable(key).value; } else { return 0f; } } /** * Sets the value of a float variable. * The variable must already be added to the list of variables for this Flowchart. */ public virtual void SetFloatVariable(string key, float value) { FloatVariable variable = GetVariable(key); if (variable != null) { variable.value = value; } } /** * Gets the value of a string variable. * Returns the empty string if the variable key does not exist. */ public virtual string GetStringVariable(string key) { StringVariable variable = GetVariable(key); if (variable != null) { return GetVariable(key).value; } else { return ""; } } /** * Sets the value of a string variable. * The variable must already be added to the list of variables for this Flowchart. */ public virtual void SetStringVariable(string key, string value) { StringVariable variable = GetVariable(key); if (variable != null) { variable.value = value; } } /** * Set the block objects to be hidden or visible depending on the hideComponents property. */ public virtual void UpdateHideFlags() { if (hideComponents) { Block[] blocks = GetComponents(); foreach (Block block in blocks) { block.hideFlags = HideFlags.HideInInspector; if (block.gameObject != gameObject) { block.gameObject.hideFlags = HideFlags.HideInHierarchy; } } Command[] commands = GetComponents(); foreach (Command command in commands) { command.hideFlags = HideFlags.HideInInspector; } EventHandler[] eventHandlers = GetComponents(); foreach (EventHandler eventHandler in eventHandlers) { eventHandler.hideFlags = HideFlags.HideInInspector; } } else { MonoBehaviour[] monoBehaviours = GetComponents(); foreach (MonoBehaviour monoBehaviour in monoBehaviours) { if (monoBehaviour == null) { continue; } monoBehaviour.hideFlags = HideFlags.None; monoBehaviour.gameObject.hideFlags = HideFlags.None; } } } public virtual void ClearSelectedCommands() { selectedCommands.Clear(); } public virtual void AddSelectedCommand(Command command) { if (!selectedCommands.Contains(command)) { selectedCommands.Add(command); } } public virtual void Reset(bool resetCommands, bool resetVariables) { if (resetCommands) { Command[] commands = GetComponents(); foreach (Command command in commands) { command.OnReset(); } } if (resetVariables) { foreach (Variable variable in variables) { variable.OnReset(); } } } /** * Override this in a Flowchart subclass to filter which commands are shown in the Add Command list. */ public virtual bool IsCommandSupported(CommandInfoAttribute commandInfo) { foreach (string key in hideCommands) { // Match on category or command name (case insensitive) if (String.Compare(commandInfo.Category, key, StringComparison.OrdinalIgnoreCase) == 0 || String.Compare(commandInfo.CommandName, key, StringComparison.OrdinalIgnoreCase) == 0) { return false; } } return true; } /** * Returns true if there are any executing blocks in this Flowchart. */ public virtual bool HasExecutingBlocks() { Block[] blocks = GetComponents(); foreach (Block block in blocks) { if (block.IsExecuting()) { return true; } } return false; } /** * Returns a list of all executing blocks in this Flowchart. */ public virtual List GetExecutingBlocks() { List executingBlocks = new List(); Block[] blocks = GetComponents(); foreach (Block block in blocks) { if (block.IsExecuting()) { executingBlocks.Add(block); } } return executingBlocks; } /** * Implementation of StringSubstituter.ISubstitutionHandler which matches any public variable in the Flowchart. * To perform full variable substitution with all substitution handlers in the scene, you should * use the SubstituteVariables() method instead. */ [MoonSharp.Interpreter.MoonSharpHidden] public virtual bool SubstituteStrings(StringBuilder input) { // Instantiate the regular expression object. Regex r = new Regex("{\\$.*?}"); bool modified = false; // Match the regular expression pattern against a text string. var results = r.Matches(input.ToString()); foreach (Match match in results) { string key = match.Value.Substring(2, match.Value.Length - 3); // Look for any matching public variables in this Flowchart foreach (Variable variable in variables) { if (variable == null) continue; if (variable.Scope == VariableScope.Public && variable.Key == key) { string value = variable.ToString(); input.Replace(match.Value, value); modified = true; } } } return modified; } /** * Substitute variables in the input text with the format {$VarName} * This will first match with private variables in this Flowchart, and then * with public variables in all Flowcharts in the scene (and any component * in the scene that implements StringSubstituter.ISubstitutionHandler). */ public virtual string SubstituteVariables(string input) { if (stringSubstituer == null) { stringSubstituer = new StringSubstituter(); } // Use the string builder from StringSubstituter for efficiency. StringBuilder sb = stringSubstituer._StringBuilder; sb.Length = 0; sb.Append(input); // Instantiate the regular expression object. Regex r = new Regex("{\\$.*?}"); bool changed = false; // Match the regular expression pattern against a text string. var results = r.Matches(input); foreach (Match match in results) { string key = match.Value.Substring(2, match.Value.Length - 3); // Look for any matching private variables in this Flowchart first foreach (Variable variable in variables) { if (variable == null) continue; if (variable.Scope == VariableScope.Private && variable.Key == key) { string value = variable.ToString(); sb.Replace(match.Value, value); changed = true; } } } // Now do all other substitutions in the scene changed |= stringSubstituer.SubstituteStrings(sb); if (changed) { return sb.ToString(); } else { return input; } } } }