Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Last active May 12, 2025 04:33
Show Gist options
  • Save adammyhre/af9eabff54f2402fe206fcb550f82cb0 to your computer and use it in GitHub Desktop.
Save adammyhre/af9eabff54f2402fe206fcb550f82cb0 to your computer and use it in GitHub Desktop.
MVC Ability System
[CreateAssetMenu(fileName = "AbilityData", menuName = "ScriptableObjects/AbilityData", order = 1)]
public class AbilityData : ScriptableObject {
public AnimationClip animationClip;
public int animationHash;
public float duration;
public Sprite icon;
public string fullName;
void OnValidate() {
animationHash = Animator.StringToHash(animationClip.name);
}
}
public class AbilityModel {
public readonly ObservableList<Ability> abilities = new();
public void Add(Ability a) {
abilities.Add(a);
}
}
public class Ability {
public readonly AbilityData data;
public Ability(AbilityData data) {
this.data = data;
}
public AbilityCommand CreateCommand() {
return new AbilityCommand(data);
}
}
public interface ICommand {
void Execute();
}
public class AbilityCommand : ICommand {
private readonly AbilityData data;
public float duration => data.duration;
public AbilityCommand(AbilityData data) {
this.data = data;
}
public void Execute() {
EventBus<PlayerAnimationEvent>.Raise(new PlayerAnimationEvent {
animationHash = data.animationHash
});
}
}
public struct PlayerAnimationEvent : IEvent {
public int animationHash;
}
public class AbilityButton : MonoBehaviour {
public Image radialImage;
public Image abilityIcon;
public int index;
public Key key;
public event Action<int> OnButtonPressed = delegate { };
void Start() {
GetComponent<Button>().onClick.AddListener(() => OnButtonPressed(index));
}
void Update() {
if (Keyboard.current[key].wasPressedThisFrame) {
OnButtonPressed(index);
}
}
public void RegisterListener(Action<int> listener) {
OnButtonPressed += listener;
}
public void Initialize(int index, Key key) {
this.key = key;
this.index = index;
}
public void UpdateButtonSprite(Sprite newIcon) {
abilityIcon.sprite = newIcon;
}
public void UpdateRadialFill(float progress) {
if (radialImage) {
radialImage.fillAmount = progress;
}
}
}
public class AbilityView : MonoBehaviour {
[SerializeField] public AbilityButton[] buttons;
readonly Key[] keys = { Key.Digit1, Key.Digit2, Key.Digit3, Key.Digit4, Key.Digit5 };
void Awake() {
for (int i = 0; i < buttons.Length; i++) {
if (i >= keys.Length) {
Debug.LogError("Not enough keycodes for the number of buttons.");
break;
}
buttons[i].Initialize(i, keys[i]);
UpdateRadial(0);
}
}
public void UpdateRadial(float progress) {
if (float.IsNaN(progress)) {
progress = 0;
}
Array.ForEach(buttons, button => button.UpdateRadialFill(progress));
}
public void UpdateButtonSprites(IList<Ability> abilities) {
for (int i = 0; i < buttons.Length; i++) {
if (i < abilities.Count) {
buttons[i].UpdateButtonSprite(abilities[i].data.icon);
} else {
buttons[i].gameObject.SetActive(false);
}
}
}
}
public class AbilityController {
readonly AbilityModel model;
readonly AbilityView view;
readonly Queue<AbilityCommand> abilityQueue = new();
readonly CountdownTimer timer = new CountdownTimer(0);
AbilityController(AbilityView view, AbilityModel model) {
this.view = view;
this.model = model;
ConnectModel();
ConnectView();
}
void ConnectModel() {
model.abilities.AnyValueChanged += UpdateButtons;
}
void ConnectView() {
for (int i = 0; i < view.buttons.Length; i++) {
view.buttons[i].RegisterListener(OnAbilityButtonPressed);
}
view.UpdateButtonSprites(model.abilities);
}
public void Update(float deltaTime) {
timer.Tick(deltaTime);
view.UpdateRadial(timer.Progress);
if (!timer.IsRunning && abilityQueue.TryDequeue(out AbilityCommand cmd)) {
cmd.Execute();
timer.Reset(cmd.duration);
timer.Start();
}
}
void UpdateButtons(IList<Ability> updatedAbilities) => view.UpdateButtonSprites(updatedAbilities);
void OnAbilityButtonPressed(int index) {
if (timer.Progress < 0.25f || !timer.IsRunning) {
if (model.abilities[index] != null) {
abilityQueue.Enqueue(model.abilities[index].CreateCommand());
}
}
EventSystem.current.SetSelectedGameObject(null);
}
public class Builder {
readonly AbilityModel model = new AbilityModel();
public Builder WithAbilities(AbilityData[] datas) {
foreach (var data in datas) {
model.Add(new Ability(data));
}
return this;
}
public AbilityController Build(AbilityView view) {
Preconditions.CheckNotNull(view);
return new AbilityController(view, model);
}
}
}
public class AbilitySystem : MonoBehaviour {
[SerializeField] AbilityView view;
[SerializeField] AbilityData[] startingAbilities;
AbilityController controller;
void Awake() {
controller = new AbilityController.Builder()
.WithAbilities(startingAbilities)
.Build(view);
}
void Update() => controller.Update(Time.deltaTime);
}
@rinnocenti
Copy link

I'm trying to use this system with the advented Timer, but it's starting to bug.
When you click on an ability, all the radials activate together.
Each button performs its action, but the cooldown is activated for all buttons.

I would probably have to dynamically create a timer for each button created, but they would need a separate index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment