Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Last active November 7, 2024 12: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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment