using System; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; /// /// Class to handle and draw a nice line graph to a Texture2D /// public class ProgressGraph : MonoBehaviour { /// /// UI reference to the plot /// public RawImage progressGraph; /// /// Prefab of the highscore marker to display on the graph /// public GameObject highscoreMarker; /// /// Prefab of the axes tick marker to display on the graph /// public GameObject axesTickMarker; /// /// Color of the graph line /// public Color lineColor; /// /// Bckground color /// public Color backgroundColor; /// /// Color of the text and axes grid /// public Color textColor; /// /// Color of the highscore line /// public Color highscoreColor; /// /// Left and right padding of the graph /// private const int GRAPH_PADDING_X_PX = 50; /// /// Top and bottom padding of the graph /// private const int GRAPH_PADDING_Y_PX = 50; /// /// Radius of the point on the graph /// private const int GRAPH_POINT_RADIUS = 10; /// /// Size of the line on the graph /// private const int GRAPH_LINE_SIZE = 4; /// /// Plot points and a highscore on the graph /// /// List of score values to plot /// Highscore value (this will be plotted in a fancy color) public void Plot(List 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> points = new List>(); 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().localPosition = new Vector3(-10 - rect.sizeDelta.y * rect.pivot.x, y - 25 - rect.sizeDelta.y * rect.pivot.y, 0); TMP_Text txt = tick.GetComponent(); 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().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 p = points[i]; if (0 < i) { Tuple 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; } /// /// Calculate nice spacing /// /// Either `max - min` if max != min, otherwise `highscore` /// Number of horizontal axes grid lines shown on the graph /// Spacing between each axes grid line 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; } /// /// Set all the pixels of a texture to a given color /// /// Texture to fill /// Color to set the texture to 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); } /// /// Draw a point to a texture /// /// Texture2D to plot point on /// Center x-pos /// Center y-pos /// Radius (aka width and height) /// Color of the point 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); } /// /// Draw a line to a texture /// /// Texture2D to plot line on /// Starting x-pos /// Strating y-pos /// Ending x-pos /// Ending y-pos /// Size of the line (width) /// Color of the line 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; } } }