// Start the conversation if possible public void StartConversation(Conversation conversation) { if (_inConversation) { DialogueLogger.LogWarning($"Trying to start a conversation while already in a conversation"); return; } if (_uiController == null) { DialogueLogger.LogError("Trying to start a conversation, but there's not DialogueUIController assigned"); return; } // Find starting point _currentConversation = null; _currentDialogue = null; _currentSentence = 0; foreach (var diag in conversation.Dialogues) { if ((_currentDialogue == null || diag.Id > _currentDialogue.Id) && diag.CanBeUsedAsStartingPoint) { if (diag.StartConditions.Count > 0) { if (diag.EvaluateStartingConditions()) { _currentDialogue = diag; } } else { _currentDialogue = diag; } } } if (_currentDialogue == null) { DialogueLogger.LogError($"Couldn't find starting point for conversation"); return; } // Start the conversation _inConversation = true; _currentConversation = conversation; if (needToChangeTheme()) { ChangeTheme(_currentDialogue.Theme); } _uiController.ShowSentence(_currentDialogue.SpeakersName, parseSentenceForCustomTags(_currentDialogue.Sentences[_currentSentence]), SpriteRepo.Instance.RetrieveSprite(_currentDialogue.CharacterSpritesName, string.IsNullOrEmpty(_currentDialogue.StartingSprite) ? "Default" : _currentDialogue.StartingSprite), _currentDialogue.AutoProceed); ConversationStarted?.Invoke(); }
// Add a theme to the repo if it's valid public void RegisterThemeSprites(Theme_SO sprites) { if (_themeSprites.ContainsKey(sprites.Name)) { DialogueLogger.LogWarning($"Theme sprites for {sprites.Name} already exist, overwritting"); } if (validateSprites(sprites)) { _themeSprites[sprites.Name] = sprites; } }
// Get the variable if it exists and we don't care about the type public object Retrieve(string name) { if (!_variables.ContainsKey(name)) { DialogueLogger.LogWarning($"Trying to retrieve the variable {name} but it hasn't been registered"); return(null); } else { return(_variables[name].Value); } }
// Add the sprites to the repo ready for retrieval public void RegisterCharacterSprites(DialogueSprites_SO sprites) { if (_characterSprites.ContainsKey(sprites.CharactersName)) { DialogueLogger.LogWarning($"Character sprites for {sprites.CharactersName} already exist, overwritting"); } if (validateSprites(sprites)) { _characterSprites[sprites.CharactersName] = sprites; } }
// Return the variable if it exists public T Retrieve <T>(string name) { // I don't like returning a default like this, but it's better than an exception if (!_variables.ContainsKey(name)) { DialogueLogger.LogWarning($"Trying to retrieve the variable {name} but it hasn't been registered. Returning default"); return(default(T)); } else { return(_variables[name].GetValue <T>()); } }
// Validate then execute private static void performAction(DialogueAction action) { if (!validateAction(action)) { return; } switch (action.ActionType) { case DialogueAction.Types.LOG: DialogueLogger.Log(action.Message); break; case DialogueAction.Types.LOG_WARNING: DialogueLogger.LogWarning(action.Message); break; case DialogueAction.Types.LOG_ERROR: DialogueLogger.LogError(action.Message); break; case DialogueAction.Types.CLOSE_CONVERSATION: DialogueController.Instance?.StopCurrentConversation(); break; case DialogueAction.Types.SEND_MESSAGE: var targetObject = GameObject.Find(action.Target); if (targetObject == null) { DialogueLogger.LogError($"Trying to execute a send message action, but GameObject {action.Target} was not found. Skipping action"); return; } targetObject.SendMessage(action.Message, SendMessageOptions.DontRequireReceiver); break; case DialogueAction.Types.CHANGE_THEME: DialogueController.Instance?.ChangeTheme(action.Message); break; case DialogueAction.Types.CLOSE_BG_CONVERSATIONS: BackgroundDialogueController.Instance?.CloseConversations(); break; case DialogueAction.Types.START_BG_CONVERSATION: BackgroundDialogueController.Instance?.StartConversation(action.Message); break; default: DialogueLogger.LogError($"Action with the name {action.Name} has na unrecognised action type {action.ActionType}. The action type loaded from the conversation JSON is {action.Type}. Skipping action"); break; } }
// Register and validate a conversation in real-time public void RegisterConversation(string name, TextAsset file) { var tempObject = _deserializer.Deserialize <Conversation>(file.text); if (tempObject == null) { DialogueLogger.LogError($"There was an error deserializing file {file.name}"); return; } tempObject.PreValidation(); if (valdateConversation(tempObject, name)) { if (_conversations.ContainsKey(name)) { DialogueLogger.LogWarning($"Conversation {name} already registered, overwritting"); } _conversations[name] = tempObject; tempObject.FinishedParsing(); } }
// Test the variables according to the comparison string public bool Evaluate() { // Check the comparer if (_comparer == null) { DialogueLogger.LogError("Trying to evaluate a condition, but the comparison operator is null"); return(false); } // Get values var var1 = Variables[0].GetValue(); var var2 = Variables[1].GetValue(); if (var1 == null || var2 == null) { DialogueLogger.LogWarning($"Trying to evaluate a condition but one or more of the variables are null"); return(false); } // Evaluate // We're using dynamics here, so be careful. We'll have to validate types on the conversation load return(_comparer.Execute(var1, var2)); }
// Check all the values are valid private bool validateSprites(Theme_SO sprites) { var names = new List <string>(); if (sprites.ThemeSprites.Count == 0) { DialogueLogger.LogWarning($"Trying to register theme sprites for {sprites.Name} but the sprite list is empty"); } foreach (var spritePair in sprites.ThemeSprites) { // Check name if (string.IsNullOrEmpty(spritePair.Name) || string.IsNullOrWhiteSpace(spritePair.Name)) { DialogueLogger.LogError($"Error registering theme sprites for {sprites.Name}, one or more of the names is empty"); return(false); } // Check for duplicate names if (names.Contains(spritePair.Name)) { DialogueLogger.LogError($"Error registering theme sprites for {sprites.Name}, there is already a sprite with the name {spritePair.Name}. Each sprite must have a unique name"); return(false); } names.Add(spritePair.Name); // Check sprites if (spritePair.Sprite == null) { DialogueLogger.LogError($"Error registering theme sprites for {sprites.Name} with the sprite name {spritePair.Name}, but the sprite is empty"); return(false); } } return(true); }
// If you're UI supports tags, use this to execute all tags at a given position in the sentence // I don't like this here. But unfortunately, the default and background conversations are too different (in my example implementation) to put it in a parent class. // Please note, not all tags are used here protected IEnumerator processTagsForPosition(TextModifications textMod, int index) { var mods = textMod.GetAnyTextModsForPosition(index); // Check for custom modifications foreach (var mod in mods) { // Commands if (mod.ModType == TextModifications.Modifications.CLOSE_BG_CONVERSATIONS) { BackgroundDialogueController.Instance?.CloseConversations(); } // Simple modifications e.g. <command=value> if (mod.ModType == TextModifications.Modifications.SPEED) { _speedMultiplyer = (mod as SimpleModification).GetValue <float>(); } else if (mod.ModType == TextModifications.Modifications.REMOVE_VARAIBLE) { VariableRepo.Instance?.Remove((mod as SimpleModification).GetValue <string>()); } else if (mod.ModType == TextModifications.Modifications.WAIT) { yield return(new WaitForSeconds((mod as SimpleModification).GetValue <float>())); } else if (mod.ModType == TextModifications.Modifications.ACTION) { performAction((mod as SimpleModification).GetValue <string>()); } else if (mod.ModType == TextModifications.Modifications.LOG) { DialogueLogger.Log((mod as SimpleModification).GetValue <string>()); } else if (mod.ModType == TextModifications.Modifications.LOG_WARNING) { DialogueLogger.LogWarning((mod as SimpleModification).GetValue <string>()); } else if (mod.ModType == TextModifications.Modifications.LOG_ERROR) { DialogueLogger.LogError((mod as SimpleModification).GetValue <string>()); } else if (mod.ModType == TextModifications.Modifications.BG_CONVERSATION) { BackgroundDialogueController.Instance?.StartConversation((mod as SimpleModification).GetValue <string>()); } // Complex modifications e.g. <command=value>content</command> else if (mod.ModType == TextModifications.Modifications.SEND_MESSAGE) { var revievingObject = GameObject.Find((mod as SimpleModification).GetValue <string>()); if (revievingObject == null) { DialogueLogger.LogError($"Trying to execute a send message command, but GameObject {(mod as SimpleModification).GetValue<string>()} was not found"); continue; } revievingObject.SendMessage((mod as ComplexModification).GetContent <string>(), SendMessageOptions.DontRequireReceiver); } else if (mod.ModType == TextModifications.Modifications.ACTION_WITH_MESSAGE) { performActionWithMessage((mod as SimpleModification).GetValue <string>(), (mod as ComplexModification).GetContent <string>()); } else if (mod.ModType == TextModifications.Modifications.ACTION_WITH_TARGET) { performActionWithTarget((mod as SimpleModification).GetValue <string>(), (mod as ComplexModification).GetContent <string>()); } } }
// Do simple validation on the conversation e.g. That it's not empy, that all nextIds go somewhere, things like that // TODO: Move all validation code to the relevant class in the Conversation.cs private bool valdateConversation(Conversation tempConversation, string fileName) { var dialogueIds = new List <int>(); var actionNames = new List <string>(); // Get all the action names for this conversation foreach (var action in tempConversation.Actions) { // Check we at least have a name and type if (string.IsNullOrEmpty(action.Name) || string.IsNullOrWhiteSpace(action.Name) || string.IsNullOrEmpty(action.Type) || string.IsNullOrWhiteSpace(action.Type)) { DialogueLogger.LogError($"An action in file {fileName} has an empty name or type value"); return(false); } // Check if it already exists if (actionNames.Contains(action.Name)) { DialogueLogger.LogError($"An action in file {fileName} does not have a unique name"); return(false); } actionNames.Add(action.Name); // Check action type and add name to the list // Errors in DialogueAction class if (!action.GetActionType()) { return(false); } // An action's message and target can be set in real time via the dialogue, so it'll have to be verified in the DialogueController. // Verifiy all of the essential values here } // Check number of dialogues if (tempConversation.Dialogues.Count == 0) { DialogueLogger.LogError($"Empty conversation in file {fileName}"); return(false); } // Check dialogues sentences and populate Id list foreach (var diag in tempConversation.Dialogues) { // Check sentence count if (diag.Sentences.Count == 0) { DialogueLogger.LogError($"Dialogue in file {fileName} has no sentences"); return(false); } // Check sentence for empty foreach (var sentence in diag.Sentences) { if (string.IsNullOrEmpty(sentence)) { DialogueLogger.LogError($"Empty sentence in file {fileName}"); return(false); } } // Check CharacterSpriteNames and StartingSprites // Check the there's a CharacterSpriteName if we've sepecified a StartingSprite if (!string.IsNullOrEmpty(diag.StartingSprite) && string.IsNullOrEmpty(diag.CharacterSpritesName)) { DialogueLogger.LogError($"Dialogue in file {fileName} has a startingSprite but no characterSpritesName is specified"); return(false); } // Check the OnFinishedActionNames // Loop through all of the action and make sure they exist foreach (var action in diag.OnFinishedActionNames) { if (!actionNames.Contains(action)) { DialogueLogger.LogError($"An dialogues OnFinishActions in file {fileName} attemps to call an action with the name {action} that doesn't exist."); return(false); } } dialogueIds.Add(diag.Id); } // Loop through dialogues and check Ids, Conditions, and Options var startingPriorities = new List <int>(); foreach (var diag in tempConversation.Dialogues) { // Check for dialogue next Ids values if (diag.NextId != -1) { if (!dialogueIds.Contains(diag.NextId)) { DialogueLogger.LogError($"Dialoge in file {fileName} references a dialogue by id {diag.NextId} that doesn't exist"); return(false); } } // Check option texts and nextIds foreach (var option in diag.Options) { // Check if any of the option has an action // loop through all of the action and make sure they exist foreach (var action in option.SelectedActionNames) { if (!actionNames.Contains(action)) { DialogueLogger.LogError($"An option's selectedAction in file {fileName} attemps to call an action with the name {action} that doesn't exist."); return(false); } } // Check text if (string.IsNullOrEmpty(option.Text)) { DialogueLogger.LogError($"Empty option in file {fileName}"); return(false); } // Check the next dialogue exists if there's no action if (option.SelectedActionNames.Count == 0 && (!dialogueIds.Contains(option.NextId) || option.NextId == -1)) { DialogueLogger.LogError($"An option in file {fileName} has a nextId {option.NextId} that doesn't exist. If an option hasn't got an action, it needs a nextId"); return(false); } } // Check Conditions and Variables foreach (var con in diag.StartConditions) { // Can only test 2 varaibles against each other. When a conversation is picked all conditions must evaluate to true, so if more tests are needed just stack conditions if (con.Variables.Count != 2) { DialogueLogger.LogError($"A condition in file {fileName} does not contains {con.Variables.Count} variables, must have 2"); return(false); } // Check the type, value, and name individually foreach (var variable in con.Variables) { // Check there's a value if we're not retrieving from the repo if (string.IsNullOrEmpty(variable.Value)) { if (!variable.FromRepo) { DialogueLogger.LogError($"A condition variable in file {fileName} has a conditional varialbe without a value. One is required if not retrieving from the variable repo"); return(false); } } else { if (variable.FromRepo) { DialogueLogger.LogWarning($"A condition variable in file {fileName} has a value it will be ignored as the variable is marked to be retrieved from the variable repo"); } } // A type is required if we're not retrieving from the repo if (string.IsNullOrEmpty(variable.Type)) { if (!variable.FromRepo) { DialogueLogger.LogError($"A condition in file {fileName} has a conditional variable without a type. One is required if not retrieving from the variable repo"); return(false); } } else { if (variable.FromRepo) { DialogueLogger.LogWarning($"A condition variable in file {fileName} has a type but will be ignored as the variable is marked to be retrieved from the variable repo"); } } // Check there's a name if we're retrieving from the repo if (string.IsNullOrEmpty(variable.Name)) { if (variable.FromRepo) { DialogueLogger.LogError($"A condition variable in the file {fileName} does not have a name value, one is required for retrieval from the varialbe repo"); return(false); } } else { if (!variable.FromRepo) { DialogueLogger.LogWarning($"A condition variable in the file {fileName} has name value but will be ignored as the variable is not marked to be retrieved from the variable repo"); } } } var var1LowerType = con.Variables[0].Type?.ToLower(); var var2LowerType = con.Variables[1].Type?.ToLower(); // Check the comparison operator if either of the types is a string or bool if (var1LowerType == "string" || var1LowerType == "bool" || var2LowerType == "string" || var2LowerType == "bool") { if (con.Comparison != "==" && con.Comparison != "!=") { DialogueLogger.LogError($"String and booleans can only use the == and != equality operators. Condition in file {fileName} is trying to use {con.Comparison}"); return(false); } } // String and bool are only tested against each other. This isn't quite true, normally, but it's a lot more readable to only allow string and bools to be compared against each other if (!con.Variables[0].FromRepo && (var1LowerType == "bool" || var1LowerType == "string") && !con.Variables[1].FromRepo && (var2LowerType == "bool" || var2LowerType == "string")) { // Check they're compatible if (var1LowerType != var2LowerType) { DialogueLogger.LogError($"Conversation file {fileName} has a condition with an unsupported variable comparison. Strings or booleans can only be tested against each other"); return(false); } } } // Warn if autoProceed is false and the conversation type is background, we'll autoproceed anyway if (tempConversation.ConversationType == Conversation.Types.BACKGROUND) { if (!diag.AutoProceed) { DialogueLogger.LogWarning($"Dialogue in the file {fileName} is of type background and autoProceed is false. Background conversations always autoproceed"); } } } return(true); }