using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Serialization; using System; using System.Linq; using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Fungus { /** * Visual scripting controller for the Flowchart programming language. * Flowchart objects may be edited visually using the Flowchart editor window. */ [ExecuteInEditMode] public class Flowchart : MonoBehaviour { /** * Current version used to compare with the previous version so older versions can be custom-updated from previous versions. */ public const string CURRENT_VERSION = "1.0"; /** * The name of the initial block in a new flowchart. */ public const string DEFAULT_BLOCK_NAME = "New Block"; /** * Variable to track flowchart's version and if initial set up has completed. */ [HideInInspector] public string version; /** * Scroll position of Flowchart editor window. */ [HideInInspector] public Vector2 scrollPos; /** * Scroll position of Flowchart variables window. */ [HideInInspector] public Vector2 variablesScrollPos; /** * Show the variables pane. */ [HideInInspector] public bool variablesExpanded = true; /** * Height of command block view in inspector. */ [HideInInspector] public float blockViewHeight = 400; /** * Zoom level of Flowchart editor window */ [HideInInspector] public float zoom = 1f; /** * Scrollable area for Flowchart editor window. */ [HideInInspector] public Rect scrollViewRect; /** * Currently selected block in the Flowchart editor. */ [HideInInspector] [FormerlySerializedAs("selectedSequence")] public Block selectedBlock; /** * Currently selected command in the Flowchart editor. */ [HideInInspector] public List selectedCommands = new List(); /** * The list of variables that can be accessed by the Flowchart. */ [HideInInspector] public List variables = new List(); [TextArea(3, 5)] [Tooltip("Description text displayed in the Flowchart editor window")] public string 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.")] public float stepPause = 0f; /** * 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")] public bool colorCommands = true; /** * 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")] public 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.")] public bool saveSelection = true; /** * 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.")] public string localizationId = ""; /** * 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.")] public List hideCommands = new List(); /** * Position in the center of all blocks in the flowchart. */ [NonSerialized] public Vector2 centerPosition = Vector2.zero; /** * Cached list of flowchart objects in the scene for fast lookup */ public static List cachedFlowcharts = new List(); protected static bool eventSystemPresent; /** * 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 = GetComponentsInChildren(); foreach (Block block in blocks) { maxId = Math.Max(maxId, block.itemId); } Command[] commands = GetComponentsInChildren(); foreach (Command command in commands) { maxId = Math.Max(maxId, command.itemId); } return maxId + 1; } protected virtual void OnLevelWasLoaded(int level) { // 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(); } 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 = GetComponentsInChildren(); foreach (Block block in blocks) { if (block.itemId == -1 || usedIds.Contains(block.itemId)) { block.itemId = NextItemId(); } usedIds.Add(block.itemId); } Command[] commands = GetComponentsInChildren(); 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 = GetComponentsInChildren(); // 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); } } } private void UpdateVersion() { // If versions match, then we are already using the latest. if (version == CURRENT_VERSION) return; switch (version) { // Version never set, so we are initializing on first creation or this flowchart is pre-versioning. case null: case "": Initialize(); break; } version = CURRENT_VERSION; } protected virtual void Initialize() {} 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.x = position.x; b.nodeRect.y = position.y; 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 = GetComponentsInChildren(); foreach (Block block in blocks) { if (block.blockName == blockName) { return block; } } return null; } /** * Start running another Flowchart by executing a specific child block. * The block must be in an idle state to be executed. * You can use this method in a UI event. e.g. to handle a button click. */ public virtual void ExecuteBlock(string blockName) { Block [] blocks = GetComponentsInChildren(); foreach (Block block in blocks) { if (block.blockName == blockName) { ExecuteBlock(block); } } } /** * 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 = GetComponentsInChildren(); 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 = GameObject.FindObjectsOfType(); foreach (MessageReceived eventHandler in eventHandlers) { eventHandler.OnSendFungusMessage(messageName); } } /** * Start executing a specific child block in the flowchart. * The block must be in an idle state to be executed. * Returns true if the Block started execution. */ public virtual bool ExecuteBlock(Block block, Action onComplete = null) { // Block must be a component of the Flowchart game object if (block == null || block.gameObject != gameObject) { return false; } // Can't restart a running block, have to wait until it's idle again if (block.IsExecuting()) { return false; } // Execute the first command in the command list block.Execute(onComplete); return true; } /** * Stop all executing Blocks in this Flowchart. */ public virtual void StopAllBlocks() { Block [] blocks = GetComponentsInChildren(); foreach (Block block in blocks) { if (block.IsExecuting()) { block.Stop(); } } } /** * 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 = GetComponentsInChildren(); 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 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.key == key) { return variable as T; } } return null; } /** * 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.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) { foreach (Variable v in variables) { if (v.key == key) { BooleanVariable variable = v as BooleanVariable; if (variable != null) { return variable.value; } } } Debug.LogWarning("Boolean variable " + key + " not found."); 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) { foreach (Variable v in variables) { if (v.key == key) { BooleanVariable variable = v as BooleanVariable; if (variable != null) { variable.value = value; return; } } } Debug.LogWarning("Boolean variable " + key + " not found."); } /** * Gets the value of an integer variable. * Returns 0 if the variable key does not exist. */ public virtual int GetIntegerVariable(string key) { foreach (Variable v in variables) { if (v.key == key) { IntegerVariable variable = v as IntegerVariable; if (variable != null) { return variable.value; } } } Debug.LogWarning("Integer variable " + key + " not found."); 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) { foreach (Variable v in variables) { if (v.key == key) { IntegerVariable variable = v as IntegerVariable; if (variable != null) { variable.value = value; return; } } } Debug.LogWarning("Integer variable " + key + " not found."); } /** * Gets the value of a float variable. * Returns 0 if the variable key does not exist. */ public virtual float GetFloatVariable(string key) { foreach (Variable v in variables) { if (v.key == key) { FloatVariable variable = v as FloatVariable; if (variable != null) { return variable.value; } } } Debug.LogWarning("Float variable " + key + " not found."); 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) { foreach (Variable v in variables) { if (v.key == key) { FloatVariable variable = v as FloatVariable; if (variable != null) { variable.value = value; return; } } } Debug.LogWarning("Float variable " + key + " not found."); } /** * Gets the value of a string variable. * Returns the empty string if the variable key does not exist. */ public virtual string GetStringVariable(string key) { foreach (Variable v in variables) { if (v.key == key) { StringVariable variable = v as StringVariable; if (variable != null) { return variable.value; } } } Debug.LogWarning("String variable " + key + " not found."); 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) { foreach (Variable v in variables) { if (v.key == key) { StringVariable variable = v as StringVariable; if (variable != null) { variable.value = value; return; } } } Debug.LogWarning("String variable " + key + " not found."); } /** * Set the block objects to be hidden or visible depending on the hideComponents property. */ public virtual void UpdateHideFlags() { if (hideComponents) { Block[] blocks = GetComponentsInChildren(); foreach (Block block in blocks) { block.hideFlags = HideFlags.HideInInspector; if (block.gameObject != gameObject) { block.gameObject.hideFlags = HideFlags.HideInHierarchy; } } Command[] commands = GetComponentsInChildren(); foreach (Command command in commands) { command.hideFlags = HideFlags.HideInInspector; } EventHandler[] eventHandlers = GetComponentsInChildren(); foreach (EventHandler eventHandler in eventHandlers) { eventHandler.hideFlags = HideFlags.HideInInspector; } } else { MonoBehaviour[] monoBehaviours = GetComponentsInChildren(); 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 = GetComponentsInChildren(); 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; } public virtual string SubstituteVariables(string text) { string subbedText = text; // Instantiate the regular expression object. Regex r = new Regex("{\\$.*?}"); // Match the regular expression pattern against a text string. var results = r.Matches(text); foreach (Match match in results) { string key = match.Value.Substring(2, match.Value.Length - 3); // Look for any matching variables in this Flowchart first (public or private) foreach (Variable variable in variables) { if (variable == null) continue; if (variable.key == key) { string value = variable.ToString(); subbedText = subbedText.Replace(match.Value, value); } } // Now search all public variables in all scene Flowcharts in the scene foreach (Flowchart flowchart in cachedFlowcharts) { if (flowchart == this) { // We've already searched this flowchart continue; } foreach (Variable variable in flowchart.variables) { if (variable == null) continue; if (variable.scope == VariableScope.Public && variable.key == key) { string value = variable.ToString(); subbedText = subbedText.Replace(match.Value, value); } } } // Next look for matching localized string string localizedString = Localization.GetLocalizedString(key); if (localizedString != null) { subbedText = subbedText.Replace(match.Value, localizedString); } } return subbedText; } } }