最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Generic enum parameter in C#? - Stack Overflow

programmeradmin0浏览0评论

I want to make a generic class to play an animation on any 3d model

public class AnimationController {

    public void ChangeAnimation(string animation, int priority = 0, float crossfadeDuration = 0.2f, float animationSpeed = 1f) {
        if(priority is higher && this animation isn't already playing) {
            model.PlayAnimation(animation);
        }
    }

}

But instead of a string type as the first parameter, I want to use an enum, but I don't want to use a specific enum type like "FireballAnimation" as the parameter:

public void ChangeAnimation(FireballAnimation animation)

because then the class stops being modular^

And I'd prefer not calling the .ToString() method everytime I want to call ChangeAnimation()

I want the following to be possible in the same codebase:

// In the context of a fireball class:
public enum FireballAnimation {
    Casted,
    Flying,
    Impact,
}

AnimationController.ChangeAnimation(FireballAnimation.Impact);

// In the context of a Player class:
public enum PlayerAnimation {
    None,
    Idle,
    Walking,
    StrafingLeft,
    StrafingRight,
    WalkingBackwards,
    Jumping,
    Casting,
}

AnimationController.ChangeAnimation(PlayerAnimation.Jumping);

^This is only possible if the first parameter of the ChangeAnimation method is somehow of a generic enum type. Is this possible in C#?

I want to make a generic class to play an animation on any 3d model

public class AnimationController {

    public void ChangeAnimation(string animation, int priority = 0, float crossfadeDuration = 0.2f, float animationSpeed = 1f) {
        if(priority is higher && this animation isn't already playing) {
            model.PlayAnimation(animation);
        }
    }

}

But instead of a string type as the first parameter, I want to use an enum, but I don't want to use a specific enum type like "FireballAnimation" as the parameter:

public void ChangeAnimation(FireballAnimation animation)

because then the class stops being modular^

And I'd prefer not calling the .ToString() method everytime I want to call ChangeAnimation()

I want the following to be possible in the same codebase:

// In the context of a fireball class:
public enum FireballAnimation {
    Casted,
    Flying,
    Impact,
}

AnimationController.ChangeAnimation(FireballAnimation.Impact);

// In the context of a Player class:
public enum PlayerAnimation {
    None,
    Idle,
    Walking,
    StrafingLeft,
    StrafingRight,
    WalkingBackwards,
    Jumping,
    Casting,
}

AnimationController.ChangeAnimation(PlayerAnimation.Jumping);

^This is only possible if the first parameter of the ChangeAnimation method is somehow of a generic enum type. Is this possible in C#?

Share Improve this question edited Jan 19 at 13:41 MaybeAFish asked Jan 19 at 13:19 MaybeAFishMaybeAFish 236 bronze badges 4
  • 2 Why not put every animation into a single enum? – Sweeper Commented Jan 19 at 13:23
  • For code clarity, and to verify if the animation in a specific enum exists in the model that needs to be animated Is it really a good idea to have possibly dozens to hundreds of animationnames in the same enum? That would make it harder to manage and less intuitive to understand which animations belong to which entity – MaybeAFish Commented Jan 19 at 13:46
  • What do you mean by "verify if the animation in a specific enum exists in the model that needs to be animated"? Please show an example with code. – Sweeper Commented Jan 19 at 13:54
  • I'm planning something like this, so a user of the codebase gets notified if the animation doesnt exist cs if (AnimationExists(animation.ToString())) { } else { Debug.Warn($"Animation '{animation}' does not exist"); } I can make the AnimationExists method non-enum dependant, but enums will help conceptualizing to the users what animations exists in a certain model – MaybeAFish Commented Jan 19 at 14:07
Add a comment  | 

4 Answers 4

Reset to default 2

Starting with C# 7.3, you can use the where T : Enum constraint to make the method generic and ensure the parameter is an enum type. Here's how you can implement it:

public class AnimationController
{
    public void ChangeAnimation<T>(T animation, int priority = 0, float crossfadeDuration = 0.2f, float animationSpeed = 1f) 
        where T : Enum
    {
        string animationName = animation.ToString();

        // Assuming you have logic to determine if the priority is higher and the animation isn't already playing
        if (IsHigherPriority(priority) && !IsAnimationPlaying(animationName))
        {
            PlayAnimation(animationName, crossfadeDuration, animationSpeed);
        }
    }

    private bool IsHigherPriority(int priority)
    {
        // Placeholder logic for priority check
        return true;
    }

    private bool IsAnimationPlaying(string animationName)
    {
        // Placeholder logic to check if the animation is already playing
        return false;
    }

    private void PlayAnimation(string animationName, float crossfadeDuration, float animationSpeed)
    {
        // Placeholder logic to play the animation
        Console.WriteLine($"Playing animation: {animationName}, Crossfade: {crossfadeDuration}, Speed: {animationSpeed}");
    }
}

Usage Example:

public enum FireballAnimation
{
    Casted,
    Flying,
    Impact,
}

public enum PlayerAnimation
{
    None,
    Idle,
    Walking,
    StrafingLeft,
    StrafingRight,
    WalkingBackwards,
    Jumping,
    Casting,
}

class Program
{
    static void Main()
    {
        AnimationController animationController = new AnimationController();

        // Using FireballAnimation
        animationController.ChangeAnimation(FireballAnimation.Impact);

        // Using PlayerAnimation
        animationController.ChangeAnimation(PlayerAnimation.Jumping);
    }
}

Please update the following code to use the enum data type instead of the string data type

public static void Main()
{
    ChangeAnimation(TestEnum.Casted);
    ChangeAnimation(TestEnum2.Idle);
}

private static void ChangeAnimation(Enum param)
{
    //Add Your Logic
    Console.WriteLine("Enum data "+ param.ToString());
}

Your question gets my ▲ because you present something about generics that is very common. At the same time, it's something of an XY problem because in a question with the title "Generic enum parameter in C#" you immediately get to what you're "really" trying to do:

I want to make a generic class to play an animation on any 3d model

Now, I see that one of the answers is allowing you to move forward, and that's great. As something to think about, however, one good solution for the second part is that you can 1. determine what the requirements are to "play an animation on any 3d model" and then 2. require your animation classes to implement it. That would be my suggested solution, and the rest of this is just to show you what I mean (this might not match exactly what you're doing, but conceptually):

What you end up with is a signature ChangeAnimation(IAnimatable animation){...} that confidently accepts any and only classes that implement the interface and its requirements.

Minimal Example


Define an interface

public interface IAnimatable
{
    Enum? Animation { get; }
    Task Play(float crossfadeDuration = 0.2f, float animationSpeed = 1f);
}

Have your Animation classes implement the Interface

In some cases, it's handy to define a common base class.

Common base class
public abstract class AnimatableBase : IAnimatable
{
    public Enum? Animation { get; set; }

    private readonly SemaphoreSlim _ready  = new SemaphoreSlim(1, 1);

    public async Task Play(float crossfadeSeconds, float animationSpeed)
    {
        // This check prevents reentry of the same 'instance'.
        // In effect, it's a fallback because controller disallows
        // multiple animations of the same Type already.
        if (_ready.Wait(0)) // Fail safe check for "already running"
        {
            try
            {
                // Conceptually simulate "some kind of animation" taking place
                await Task.Delay(TimeSpan.FromSeconds(crossfadeSeconds));
            }
            finally
            {
                _ready.Release();
            }
        }
    }
    public override string ToString() => $"{GetType()} {Animation}";
}

Specialized Classes

public enum FireballAnimation{ Casted, Flying, Impact, }
public class Fireball : AnimatableBase
{
    public Fireball(FireballAnimation animation) => Animation = animation;
}

public enum PlayerAnimation
{ None, Idle, Walking, StrafingLeft, StrafingRight, WalkingBackwards, Jumping,  Casting, }
public class Player : AnimatableBase
{
    public Player(PlayerAnimation animation) => Animation = animation;
}

Animation Player

Conceptually, some kind of 3d animation controller loosely based on the syntax you show.

public class AnimationController
{
    private readonly Dictionary<Type, int> RunningAnimations = new();
    public async Task ChangeAnimation(IAnimatable animation, int priority = 0, float crossfadeSeconds = 0.2f, float animationSpeed = 1f)
    {
        var animationType = animation.GetType();
        int currentPriority;
        lock (_lock)
        {
            if (RunningAnimations.ContainsKey(animationType))
            {   /* G T K */
                Console.WriteLine($@"{DateTime.Now:hh\:mm\:ss} {animation.GetType().Name} is already running! Ignoring {animation}.");
                return;
            }
            currentPriority = RunningAnimations.Any() ? RunningAnimations.Values.Max() : -1;
        }
        if (priority > currentPriority)
        {
            _ready.Wait(0);
            Console.WriteLine($@"{DateTime.Now:hh\:mm\:ss} Starting {animation} @ priority {priority}");
            RunningAnimations[animationType] = priority;
            await animation.Play(crossfadeSeconds, animationSpeed);
            Console.WriteLine($@"{DateTime.Now:hh\:mm\:ss} Ending {animation}");
            RunningAnimations.Remove(animationType);
            lock(_lock)
            {
                if(!RunningAnimations.Any())
                {
                    _ready.Release();
                }
            }
        }
        else Console.WriteLine($@"{DateTime.Now:hh\:mm\:ss} Ignoring {animation}. Priority {priority} is not higher that current priority {currentPriority}.");
    }
    object _lock = new object();
    private readonly SemaphoreSlim _ready = new SemaphoreSlim(1, 1);
    public TaskAwaiter GetAwaiter() => _ready.WaitAsync().GetAwaiter();
}

Take it for a Test Drive

This is a console app to try it out.

using System.Runtime.CompilerServices;

Console.Title = "Animation Controller";
var controller = new AnimationController();

_ = controller.ChangeAnimation(new Fireball(FireballAnimation.Flying), crossfadeSeconds: 5);
_ = controller.ChangeAnimation(new Fireball(FireballAnimation.Impact));
_ = controller.ChangeAnimation(new Player(PlayerAnimation.Walking));
_ = controller.ChangeAnimation(new Player(PlayerAnimation.Jumping), crossfadeSeconds: 1, priority:1);
_ = controller.ChangeAnimation(new Fireball(FireballAnimation.Casted), crossfadeSeconds: 1, priority:1);

// Let all animations run out, then retry Fireball.Impact
await controller;
_ = controller.ChangeAnimation(new Fireball(FireballAnimation.Impact), crossfadeSeconds: 1);

Console.ReadKey();

You could use a Flags enum and compose the enum values. A base value would indicate the type of animation FireballAnimation or PlayerAnimation.

[Flags]
public enum Animation : uint
{
    FireballAnimation = 2 << 10,
    PlayerAnimation = 2 << 11,

    Casted = FireballAnimation + 1,
    Flying = FireballAnimation + 2,
    Impact = FireballAnimation + 3,

    None = PlayerAnimation + 0,
    Idle = PlayerAnimation + 1,
    Walking = PlayerAnimation + 2,
    StrafingLeft = PlayerAnimation + 3,
    StrafingRight = PlayerAnimation + 4,
    WalkingBackwards = PlayerAnimation + 5,
    Jumping = PlayerAnimation + 6,
    Casting = PlayerAnimation + 7,
}

Note: 2 << 10 is 2^10 = 1024. This is the maximum number of animations (including None) for a specific model type. You can have model types up to 2 << 30 when the enum is based on uint. If this is not enough, you can base the enum on a ulong instead and go up to 2 << 62 and also start at a number smaller than 2 << 10.

Then you can test whether an animation is valid, given a variable Animation animation, with:

if (animation.HasFlag(Animation.PlayerAnimation)) {
    StartAnimation(animation);
}

You could even include this check into an abstract model base class.

public abstract class ModelBase
{
   public abstract bool HasAnimation(Animation animation);
}

In the specific models you would implement it:

public class fireballModel : ModelBase
{
   public override bool HasAnimation(Animation animation)
       => animation.HasFlag(Animation.FireballAnimation); 
}

public class PlayerModel : ModelBase
{
   public override bool HasAnimation(Animation animation)
       => animation.HasFlag(Animation.PlayerAnimation); 
}

Then you can ask the model itself (without having to know what type of model it is):

if (model.HasAnimation(animation)) {
    AnimationController.ChangeAnimation(animation);
}

You can also implement this through an interface instead of a common base class:

public interface IAnimatable
{
    bool HasAnimation(Animation animation)
}

You can then check if a model is animatable and has an animation with

if (model is IAnimatable a && a.HasAnimation(animation)) {
    AnimationController.ChangeAnimation(animation);
}
发布评论

评论列表(0)

  1. 暂无评论