Last active
November 7, 2024 12:33
-
-
Save adammyhre/af9eabff54f2402fe206fcb550f82cb0 to your computer and use it in GitHub Desktop.
MVC Ability System
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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