Skip to content

Instantly share code, notes, and snippets.

@modality
Last active October 21, 2020 05:08
Show Gist options
  • Save modality/7fbdd466c98c2047914ff330f0fe9943 to your computer and use it in GitHub Desktop.
Save modality/7fbdd466c98c2047914ff330f0fe9943 to your computer and use it in GitHub Desktop.
using System;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ITopic {
bool IsClosed();
void Update();
void Close();
string ToString();
}
public class Topic<T> : ITopic where T : new()
{
T value = new T();
string name;
public event Action<T> channels;
public HashSet<string> aliases;
public bool everPublished;
public bool dirty;
public bool closing;
public bool closed;
public Topic(string name)
{
this.name = name;
channels = delegate { };
aliases = new HashSet<string>();
everPublished = false;
dirty = false;
closing = false;
closed = false;
}
public void UpdateValue(T value) {
if (closed) {
return;
}
if (!everPublished || !value.Equals(this.value)) {
this.value = value;
dirty = true;
everPublished = true;
}
}
public T Value() {
return value;
}
public void Update() {
if (closed) {
return;
}
Publish();
if (closing) {
closed = true;
}
}
void Publish() {
if (dirty) {
try {
channels(value);
foreach (string alias in aliases) {
Publisher.Instance.Publish<T>(alias, value);
}
} catch {
Debug.LogError("Error publishing "+this.name);
throw;
}
dirty = false;
}
}
public void Close() {
closing = true;
}
public bool IsClosed() {
return closed;
}
public void Subscribe(Action<T> listener) {
if (everPublished) {
listener(value);
}
channels += listener;
}
public void Unsubscribe(Action<T> listener) {
channels -= listener;
}
public Delegate[] Subscribers()
{
return channels.GetInvocationList();
}
public void AddAlias(string alias)
{
aliases.Add(alias);
if (everPublished) {
Publisher.Instance.Publish<T>(alias, value);
}
}
public void RemoveAlias(string alias)
{
aliases.Remove(alias);
}
public string ToString()
{
List<string> output = new List<string>();
output.Add($"[{name}]");
output.Add($"aliases: {System.String.Join(", ", aliases)}");
output.Add($"ever pub'd: {everPublished}");
output.Add($"closed: {closed}");
return System.String.Join("\n", output);
}
}
public class Publisher : MonoBehaviour {
public static Publisher Instance { get; set; }
bool dirty;
int maxFrames;
float maxTime;
float minTime;
float[] rolling;
int rollingIndex;
Dictionary<string, ITopic> topics = new Dictionary<string, ITopic>();
Dictionary<string, string> aliases = new Dictionary<string, string>();
void Awake()
{
rolling = new float[1000];
rollingIndex = 0;
if (Instance != null && Instance != this) {
Destroy(this.gameObject);
} else {
Instance = this;
}
}
void Update()
{
if (dirty) {
Publisher.Instance.Publish<List<string>>("Publisher.Topics", topics.Keys.ToList());
dirty = false;
}
}
void OnApplicationQuit()
{
double min = System.Math.Round(minTime*1000, 2);
double max = System.Math.Round(maxTime*1000, 2);
double avg = System.Math.Round(rolling.Average()*1000, 2);
Debug.Log($"PUBLISHER STATS: min - {min}ms avg - {avg}ms max - {max}ms // max frames to process {maxFrames}");
}
public void Init()
{
StartCoroutine(PublishDebounce());
}
public IEnumerator PublishDebounce() {
float maximumTimePerFrame = 1.0f / 50; // 50 fps
float timeStamp;
int frames;
bool yieldOvertime = true;
maxTime = 0f;
minTime = 1f;
maxFrames = 0;
while (true) {
timeStamp = Time.realtimeSinceStartup;
frames = 0;
// make a copy first, because calling Update on a topic may add new topics/workers
ITopic[] currentTopics = topics.Values.ToArray();
foreach(ITopic topic in currentTopics) {
topic.Update();
if (yieldOvertime && Time.realtimeSinceStartup > timeStamp + maximumTimePerFrame)
{
frames += 1;
yield return null; // wait for next frame of gameplay
timeStamp = Time.realtimeSinceStartup;
}
}
// cull closed topics
int count = topics.Count;
topics = topics.Where(t => !t.Value.IsClosed()).ToDictionary(t => t.Key, t => t.Value);
if (topics.Count < count) {
dirty = true;
}
// profilin'
float duration = Time.realtimeSinceStartup - timeStamp;
ProfileOneIteration(duration, frames);
yield return null;
}
}
public void ProfileOneIteration(float duration, int frameCount)
{
if (duration > maxTime) {
maxTime = duration;
}
if (duration < minTime) {
minTime = duration;
}
if (frameCount > maxFrames) {
maxFrames = frameCount;
}
rolling[rollingIndex] = duration;
rollingIndex += 1;
rollingIndex = rollingIndex % rolling.Length;
}
public void Subscribe<T>(string key, Action<T> action) where T : new()
{
Topic<T> topic = GetTopic<T>(key);
try {
topic.Subscribe(action);
} catch {
Debug.LogError($"Error subscribing to topic {key} (usually a type problem)");
throw;
}
}
public void Unsubscribe<T>(string key, Action<T> action) where T : new()
{
if (topics.ContainsKey(key)) {
Topic<T> topic = GetTopic<T>(key);
try {
topic.Unsubscribe(action);
} catch {
Debug.LogError($"Error unsubscribing from topic {key} (usually a type problem)");
throw;
}
}
}
public Topic<T> GetTopic<T>(string key) where T : new()
{
if (!topics.ContainsKey(key)) {
topics[key] = new Topic<T>(key);
dirty = true;
}
return topics[key] as Topic<T>;
}
public void CloseTopic(string key)
{
if (!topics.ContainsKey(key)) {
Debug.LogError($"can't close non-existent topic {key}");
return;
}
topics[key].Close();
}
public T GetTopicValue<T>(string key) where T : new()
{
return GetTopic<T>(key).Value();
}
public void Publish<T>(string key, T value) where T : new()
{
Topic<T> topic = GetTopic<T>(key);
if (value != null) {
topic.UpdateValue(value);
}
}
public void AddAlias<T>(string alias, string original) where T : new()
{
if (aliases.ContainsKey(alias)) {
RemoveAlias<T>(alias);
}
aliases[alias] = original;
GetTopic<T>(original).AddAlias(alias);
}
public void RemoveAlias<T>(string alias) where T : new()
{
if (aliases.ContainsKey(alias)) {
string original = aliases[alias];
aliases.Remove(alias);
GetTopic<T>(original).RemoveAlias(alias);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Character : MonoBehaviour
{
int health;
string healthTopic;
string characterName;
void Awake()
{
health = 3;
healthTopic = $"Character.{GetInstanceID()}.Health";
}
void Start()
{
if (characterName == "Main Character") {
Publisher.AddAlias<object>("MainCharacter.Health", healthTopic);
}
}
void OnDestroy()
{
Publisher.CloseTopic(healthTopic);
}
void TakeDamage()
{
health -= 1;
Publisher.Instance.Publish<object>(healthTopic, health);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public abstract class SimpleView : MonoBehaviour
{
// set this topic in your inspector, for example "MainCharacter.Health"
public string topic;
// this is just a UI element
public TextMeshProUGUI textObject;
public virtual void OnEnable()
{
if (topic != "") {
Publisher.Instance.Subscribe<object>(topic, RenderTemplate);
}
}
public virtual void OnDisable()
{
if (topic != "") {
Publisher.Instance.Unsubscribe<object>(topic, RenderTemplate);
}
}
public virtual void SetBinding(string topic)
{
if (this.topic != "") {
Publisher.Instance.Unsubscribe<object>(this.topic, RenderTemplate);
}
this.topic = topic;
if (this.topic != "") {
Publisher.Instance.Subscribe<object>(this.topic, RenderTemplate);
}
}
private void RenderTemplate(object incoming)
{
textObject.text = (string) incoming;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment