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