using System; using System.Text; using UnityEngine; using Framework.Services; using Framework.Support; namespace Framework.Dialogue { /// Manages a place where a dialogue canvas might be displayed /// /// /// Depending on the game, a dialogue canvas could be attached to the main camera /// or the VR player's wrist, but it would also be a viable option to let /// the level designer choose where dialogue canvases can appear in the world -- /// for example, next to a shop window, parallel to a cell phone or in a car /// between driver and passenger seat. /// /// /// This class allows the level designer to pre-determine locations for /// dialogue canvases. Each placement can have multiple spots its dialogue canvas /// can appear at and even allows a dialogue canvas to orbit around a character. /// /// public class DialogueOwner : ScriptComponent { /// Prefab that will be instantiated when a dialogue canvas is needed public DialogueCanvas OverridePrefab; /// Prefab that will be instantiated to show a preview of the dialogue UI public DialogueCanvas PreviewPrefab; /// Locations at which a dialogue canvas can appear statically public Transform[] StaticPlacements; /// Gets or creates the dialogue canvas to use in this dialogue placement /// Location and orientation the canvas will be placed at /// A dialogue canvas, placed at the specified spot public IDialogueCanvas GetOrCreateDialogueCanvas(Transform targetTransform) { DialogueCanvas canvas = GetComponentInChildren(); if(canvas == null) { if(this.OverridePrefab == null) { Debug.LogError( "Dialogue placement '" + gameObject.name + "' was asked to provide " + "a dialogue canvas but no prefab is set and no existing canvas could be found." ); return null; } canvas = Instantiate(this.OverridePrefab, targetTransform); GameObject canvasRoot = canvas.gameObject; canvasRoot.name = "RuntimeDialogueCanvas"; } else { canvas.transform.SetParent(targetTransform, false); } // Move the canvas to the target placement //Transform canvasTransform = canvas.transform; //canvasTransform.SetParent(transform); //canvasTransform.position = targetTransform.position; //canvasTransform.rotation = targetTransform.rotation; //canvasTransform.localScale = targetTransform.localScale; // Let's leave the scale intact. UGUI uses 1:1 coordinates between UI skin // texels and the world, so a texel = 1.0m, so scaling the canvas to // something like 1/1000th of a unit is expected and should be kept! //canvasTransform.localScale = Vector3.one; return canvas; } /// /// Determines the best placement of a dialogue canvas for /// the specified viewer position /// /// Position and orientation of the viewer /// /// Maximum distance the placement may have from the viewer /// /// Index of the best dialogue canvas placement public Transform GetBestPlacement( Transform viewerPosition, float maximumDistance = DialogueManager.DefaultMaximumDistance ) { Transform bestTransform = null; float bestScore = float.NaN; if(this.StaticPlacements != null) { int placementCount = this.StaticPlacements.Length; for(int index = 0; index < placementCount; ++index) { float score = ScoreStaticPlacement( viewerPosition, this.StaticPlacements[index], maximumDistance ); //Debug.Log("Placement " + index.ToString() + " scored " + score.ToString()); if(float.IsNaN(bestScore) || (score > bestScore)) { bestScore = score; bestTransform = this.StaticPlacements[index]; } } // Make sure negative scores aren't event considered if(float.IsNaN(bestScore)) { bestTransform = null; } } debugLogPlacement(viewerPosition, bestTransform, maximumDistance); return bestTransform; } /// Calculates a score that tells how good a placement is /// Position of the viewer /// Placement of the dialogue canvas that will be scored /// /// Maximum distance the placement may have from the viewer /// /// A score rating the placement of the dialogue canvas. Lower is better. /// /// A negative score means that the placement can be ruled out completely. /// public static float ScoreStaticPlacement( Transform viewer, Transform placement, float maximumDistance = DialogueManager.DefaultMaximumDistance ) { Vector3 viewerPosition = viewer.position; Vector3 placementPosition = placement.position; // How far the placement is from the viewer's position. Closer is better. // (note: there is such a thing as too close). float distanceFromViewer = Vector3.Distance(viewerPosition, placementPosition); if(distanceFromViewer >= maximumDistance) { return float.NaN; } // How much this placement is pointed at the viewer. Dialogue UI facing // away from the viewer or with its thin side to the camera doesn't work too well. Vector3 directionFacingViewer = Vector3.Normalize(viewerPosition - placementPosition); float directness = Vector3.Dot(directionFacingViewer, -placement.forward); // How much this placement is in the direction the viewer is facing. // Major influence on score since popping up dialogue behind the viewer is // bound to cause more confusion than popping up dialogue that is too far away. float centeredness = -Vector3.Dot(viewer.forward, directionFacingViewer); // Finally, the scoring formula float score = ( (maximumDistance - distanceFromViewer) * directness * // Mathf.Sqrt(directness) * (centeredness + 1.0f) ); float maximumScore = (maximumDistance * 1.0f * 2.0f); score /= maximumScore; #if UNITY_EDITOR Debug.Log( "Dialogue placement " + placement.name + "\n" + " directness = " + directness.ToString() + " centeredness = " + centeredness.ToString() + " final score = " + score.ToString() ); #endif // UNITY_EDITOR return score; } /* Nice trick getting minimum distance between line and point // Calculate how close this canvas placement is to the view's center line. float distanceToViewLine = Vector3.Cross( viewerPosition.forward, placement.position - viewerPosition.position ); */ #if UNITY_EDITOR /// Checks the game object when the component gets loaded protected virtual void Start() { DialogueLayerHelper.CheckDialoguePlacement(gameObject); } #endif /// Logs the scores for each potential dialogue placement /// Position of the viewer in the world /// Placement that has been chosen /// /// Maximum distance the placement may have from the viewer /// private void debugLogPlacement( Transform viewerPosition, Transform bestTransform, float maximumDistance ) { var messageBuilder = new StringBuilder(); messageBuilder.Append("Dialogue placement evaluation for '"); messageBuilder.Append(gameObject.name); messageBuilder.AppendLine("':"); messageBuilder.Append(' '); if(this.StaticPlacements != null) { for(int index = 0; index < this.StaticPlacements.Length; ++index) { if(ReferenceEquals(this.StaticPlacements[index], bestTransform)) { messageBuilder.Append(" *["); } else { messageBuilder.Append(" ["); } { float score = ScoreStaticPlacement( viewerPosition, this.StaticPlacements[index], maximumDistance ); messageBuilder.Append(score.ToString("f3")); } messageBuilder.Append(' '); messageBuilder.Append(this.StaticPlacements[index].name); messageBuilder.Append(']'); } } if(bestTransform == null) { messageBuilder.Append(" (fallback)"); } Debug.Log(messageBuilder.ToString()); } } } // namespace Framework.Dialogue