264 lines
8.4 KiB
C#
264 lines
8.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// Class to handle and draw a nice line graph to a Texture2D
|
|
/// </summary>
|
|
public class ProgressGraph : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// UI reference to the plot
|
|
/// </summary>
|
|
public RawImage progressGraph;
|
|
|
|
/// <summary>
|
|
/// Prefab of the highscore marker to display on the graph
|
|
/// </summary>
|
|
public GameObject highscoreMarker;
|
|
|
|
/// <summary>
|
|
/// Prefab of the axes tick marker to display on the graph
|
|
/// </summary>
|
|
public GameObject axesTickMarker;
|
|
|
|
/// <summary>
|
|
/// Color of the graph line
|
|
/// </summary>
|
|
public Color lineColor;
|
|
|
|
/// <summary>
|
|
/// Bckground color
|
|
/// </summary>
|
|
public Color backgroundColor;
|
|
|
|
/// <summary>
|
|
/// Color of the text and axes grid
|
|
/// </summary>
|
|
public Color textColor;
|
|
|
|
/// <summary>
|
|
/// Color of the highscore line
|
|
/// </summary>
|
|
public Color highscoreColor;
|
|
|
|
/// <summary>
|
|
/// Left and right padding of the graph
|
|
/// </summary>
|
|
private const int GRAPH_PADDING_X_PX = 50;
|
|
|
|
/// <summary>
|
|
/// Top and bottom padding of the graph
|
|
/// </summary>
|
|
private const int GRAPH_PADDING_Y_PX = 50;
|
|
|
|
/// <summary>
|
|
/// Radius of the point on the graph
|
|
/// </summary>
|
|
private const int GRAPH_POINT_RADIUS = 10;
|
|
|
|
/// <summary>
|
|
/// Size of the line on the graph
|
|
/// </summary>
|
|
private const int GRAPH_LINE_SIZE = 4;
|
|
|
|
/// <summary>
|
|
/// Plot points and a highscore on the graph
|
|
/// </summary>
|
|
/// <param name="scores">List of score values to plot</param>
|
|
/// <param name="highscore">Highscore value (this will be plotted in a fancy color)</param>
|
|
public void Plot(List<double> scores, double highscore)
|
|
{
|
|
// Remove previous marker(s)
|
|
foreach (Transform child in progressGraph.gameObject.transform)
|
|
Destroy(child.gameObject);
|
|
|
|
// Get texture reference
|
|
Texture2D tex = progressGraph.texture as Texture2D;
|
|
RectTransform rect = progressGraph.gameObject.transform as RectTransform;
|
|
if (tex == null)
|
|
{
|
|
tex = new Texture2D(
|
|
width: (int)rect.sizeDelta.x,
|
|
height: (int)rect.sizeDelta.y,
|
|
textureFormat: TextureFormat.ARGB32,
|
|
mipCount: 3,
|
|
linear: true
|
|
);
|
|
}
|
|
tex.filterMode = FilterMode.Point;
|
|
FillTexture(tex, backgroundColor);
|
|
|
|
// calculate positions and offsets
|
|
int x0 = GRAPH_PADDING_X_PX, x1 = tex.width - GRAPH_PADDING_X_PX;
|
|
int y0 = GRAPH_PADDING_Y_PX, y1 = tex.height - GRAPH_PADDING_Y_PX;
|
|
double min = scores.Min();
|
|
double max = scores.Max();
|
|
|
|
List<Tuple<int, int>> points = new List<Tuple<int, int>>();
|
|
for (int i = 0; i < scores.Count; i++)
|
|
{
|
|
int _x = x0 + (scores.Count > 1 ? i * ((x1 - x0) / (scores.Count - 1)) : (x1 - x0) / 2);
|
|
int _y = y0 + (int)((y1 - y0) * (min != max ? (scores[i] - min) / (max - min) : 0.5));
|
|
points.Add(Tuple.Create(_x, _y));
|
|
}
|
|
|
|
// Calculate scaling
|
|
const int NUMBER_OF_AXES = 5;
|
|
|
|
double spacing = 0.0;
|
|
if (min == max)
|
|
{
|
|
spacing = CalculateSpacing(highscore, NUMBER_OF_AXES);
|
|
max = highscore + 2 * spacing;
|
|
min = highscore - 2 * spacing;
|
|
}
|
|
|
|
spacing = CalculateSpacing(max - min, NUMBER_OF_AXES);
|
|
double begin = spacing * Math.Round(min / spacing);
|
|
|
|
// Draw axes
|
|
double pixels_per_unit = (y1 - y0) / (max - min);
|
|
double Y = begin;
|
|
int y = y0 + (int)(pixels_per_unit * (Y - min));
|
|
int total = 0;
|
|
do
|
|
{
|
|
if (y0 <= y)
|
|
{
|
|
DrawLine(tex, x0, y, x1, y, 2, textColor);
|
|
|
|
GameObject tick = GameObject.Instantiate(axesTickMarker, rect);
|
|
tick.GetComponent<RectTransform>().localPosition = new Vector3(-10 - rect.sizeDelta.y * rect.pivot.x, y - 25 - rect.sizeDelta.y * rect.pivot.y, 0);
|
|
TMP_Text txt = tick.GetComponent<TMP_Text>();
|
|
txt.text = $"{Y}";
|
|
txt.color = textColor;
|
|
}
|
|
total += 1;
|
|
Y += spacing;
|
|
y = y0 + (int)(pixels_per_unit * (Y - min));
|
|
|
|
// Fail save
|
|
if (2 * NUMBER_OF_AXES < total)
|
|
break;
|
|
} while (y <= y1);
|
|
|
|
|
|
// Draw highscore
|
|
if (min <= highscore && highscore <= max)
|
|
{
|
|
y = y0 + (int)(pixels_per_unit * (highscore - min));
|
|
DrawLine(tex, x0, y, x1, y, GRAPH_LINE_SIZE, highscoreColor);
|
|
GameObject marker = GameObject.Instantiate(highscoreMarker, rect);
|
|
marker.GetComponent<RectTransform>().localPosition = new Vector3(tex.width - 50 - rect.sizeDelta.x * rect.pivot.x, y - 25 - rect.sizeDelta.y * rect.pivot.y, 0);
|
|
}
|
|
|
|
// Draw points
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
Tuple<int, int> p = points[i];
|
|
if (0 < i)
|
|
{
|
|
Tuple<int, int> q = points[i - 1];
|
|
DrawLine(tex, p.Item1, p.Item2, q.Item1, q.Item2, GRAPH_LINE_SIZE, lineColor);
|
|
}
|
|
DrawPoint(tex, p.Item1, p.Item2, GRAPH_POINT_RADIUS, lineColor);
|
|
}
|
|
|
|
// Apply to graph GameObject
|
|
tex.Apply();
|
|
progressGraph.texture = tex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate nice spacing
|
|
/// </summary>
|
|
/// <param name="mu">Either `max - min` if max != min, otherwise `highscore`</param>
|
|
/// <param name="numberOfAxes">Number of horizontal axes grid lines shown on the graph</param>
|
|
/// <returns>Spacing between each axes grid line</returns>
|
|
private double CalculateSpacing(double mu, int numberOfAxes)
|
|
{
|
|
if (mu == 0)
|
|
return 1.0;
|
|
|
|
double[] otherSpacings = { 0.5, 1.0, 2.0, 5.0 };
|
|
|
|
int mag = (int)Math.Floor(Math.Log10(Math.Abs(mu)));
|
|
int MAG = (int)Math.Pow(10, mag);
|
|
double spacing = MAG;
|
|
foreach (double o in otherSpacings)
|
|
{
|
|
if (Math.Abs(mu - numberOfAxes * spacing) <= Math.Abs(mu - numberOfAxes * o * MAG))
|
|
continue;
|
|
spacing = o * MAG;
|
|
}
|
|
return spacing;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set all the pixels of a texture to a given color
|
|
/// </summary>
|
|
/// <param name="tex">Texture to fill</param>
|
|
/// <param name="color">Color to set the texture to</param>
|
|
private void FillTexture(Texture2D tex, Color color)
|
|
{
|
|
for (int y = 0; y < tex.height; y++)
|
|
for (int x = 0; x < tex.width; x++)
|
|
tex.SetPixel(x, y, color);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw a point to a texture
|
|
/// </summary>
|
|
/// <param name="tex">Texture2D to plot point on</param>
|
|
/// <param name="xc">Center x-pos</param>
|
|
/// <param name="yc">Center y-pos</param>
|
|
/// <param name="r">Radius (aka width and height)</param>
|
|
/// <param name="color">Color of the point</param>
|
|
private void DrawPoint(Texture2D tex, int xc, int yc, int r, Color color)
|
|
{
|
|
for (int y = yc - r; y < yc + r; y++)
|
|
for (int x = xc - r; x < xc + r; x++)
|
|
tex.SetPixel(x, y, color);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw a line to a texture
|
|
/// </summary>
|
|
/// <param name="tex">Texture2D to plot line on</param>
|
|
/// <param name="x0">Starting x-pos</param>
|
|
/// <param name="y0">Strating y-pos</param>
|
|
/// <param name="x1">Ending x-pos</param>
|
|
/// <param name="y1">Ending y-pos</param>
|
|
/// <param name="size">Size of the line (width)</param>
|
|
/// <param name="color">Color of the line</param>
|
|
private void DrawLine(Texture2D tex, int x0, int y0, int x1, int y1, int size, Color color)
|
|
{
|
|
int w = x1 - x0;
|
|
int h = y1 - y0;
|
|
|
|
int length = Mathf.Abs(x1 - x0);
|
|
if (Mathf.Abs(y1 - y0) > length)
|
|
length = Mathf.Abs(h);
|
|
|
|
double dx = w / (double)length;
|
|
double dy = h / (double)length;
|
|
|
|
double x = x0;
|
|
double y = y0;
|
|
double r = size / 2;
|
|
for (int i = 0; i <= length; i++)
|
|
{
|
|
for (int j = (int)(y - r); j < y + r; j++)
|
|
for (int k = (int)(x - r); k < x + r; k++)
|
|
tex.SetPixel(k, j, color);
|
|
|
|
x += dx;
|
|
y += dy;
|
|
}
|
|
}
|
|
}
|