3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

複合属性を考慮した三すくみ の関係を enumのキレイなコードに書き直した話 in 英語学習ソシャゲの英語物語

Last updated at Posted at 2020-09-30

どうも毎日2万人弱がプレイする
英語学習ソーシャルゲーム、英語物語の開発者、ゴン氏です。

英語物語では火属性・水属性・風属性、火水属性、水風属性、風火属性、火水風属性
の7つの属性があるのですが、
AttributeRef.png

そのダメージ計算や、属性の演算(火水パネルに反応するのは、火属性と水属性)を定義するのに、
もともと下記のような汚いコードを書いてたんですが、
一念発起してきれいなコードに書き直したのでその備忘録です。

もし、もっとCoolな方法あったら、ぜひ教えてください😊
追記:コメントでビット演算を使ったスーパーCoolな方法を教えてもらってます。
ぜひコメントまでお読み頂けると嬉しいです。

#属性を表すenum

OneEventEnumAttribute.cs
public enum OneEventEnumAttribute
{
    Dami = 0,
    Fire = 1,
    Water = 2,
    Wind = 3,
    FireWater = 4,
    WaterWind = 5,
    WindFire = 6,
    FireWaterWind = 7,
}

#以前の汚いコード

OneEventAttributeDamageLogic.cs
private float OneEventAttributeDamageLogic(OneEventEnumAttribute from, OneEventEnumAttribute to)
    {
        if (from == to)
        {
            return 1;
        }

        switch (from)
        {
            case OneEventEnumAttribute.Fire:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1f;
                    case OneEventEnumAttribute.Water:
                        return 0.5f;
                    case OneEventEnumAttribute.Wind:
                        return 1.5f;
                    case OneEventEnumAttribute.FireWater:
                        return 0.75f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1f;
                    case OneEventEnumAttribute.WindFire:
                        return 1.25f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.Water:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1.5f;
                    case OneEventEnumAttribute.Water:
                        return 1f;
                    case OneEventEnumAttribute.Wind:
                        return 0.5f;
                    case OneEventEnumAttribute.FireWater:
                        return 1.25f;
                    case OneEventEnumAttribute.WaterWind:
                        return 0.75f;
                    case OneEventEnumAttribute.WindFire:
                        return 1f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.Wind:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 0.5f;
                    case OneEventEnumAttribute.Water:
                        return 1.5f;
                    case OneEventEnumAttribute.Wind:
                        return 1f;
                    case OneEventEnumAttribute.FireWater:
                        return 1f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1.25f;
                    case OneEventEnumAttribute.WindFire:
                        return 0.75f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.FireWater:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1.25f;
                    case OneEventEnumAttribute.Water:
                        return 0.75f;
                    case OneEventEnumAttribute.Wind:
                        return 1f;
                    case OneEventEnumAttribute.FireWater:
                        return 1f;
                    case OneEventEnumAttribute.WaterWind:
                        return 0.875f;
                    case OneEventEnumAttribute.WindFire:
                        return 1.125f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.WaterWind:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 1f;
                    case OneEventEnumAttribute.Water:
                        return 1.25f;
                    case OneEventEnumAttribute.Wind:
                        return 0.75f;
                    case OneEventEnumAttribute.FireWater:
                        return 1.125f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1f;
                    case OneEventEnumAttribute.WindFire:
                        return 0.875f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.WindFire:
                switch (to)
                {
                    case OneEventEnumAttribute.Fire:
                        return 0.75f;
                    case OneEventEnumAttribute.Water:
                        return 1f;
                    case OneEventEnumAttribute.Wind:
                        return 1.25f;
                    case OneEventEnumAttribute.FireWater:
                        return 0.875f;
                    case OneEventEnumAttribute.WaterWind:
                        return 1.125f;
                    case OneEventEnumAttribute.WindFire:
                        return 1f;
                    case OneEventEnumAttribute.FireWaterWind:
                        return 1f;
                    default:
                        break;
                }

                break;
            case OneEventEnumAttribute.FireWaterWind:
                return 1f;
            default:
                return 1f;
        }

        return 1f;
}
OneEventAttributeLogic.cs
public bool CanAttack(OneEventEnumAttribute attacker, OneEventEnumAttribute panelAttributle)
    {
        switch (panelAttributle)
        {
            case OneEventEnumAttribute.Fire:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Fire:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.Water:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Water:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.Wind:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Wind:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.FireWater:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Fire:
                    case OneEventEnumAttribute.Water:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.WaterWind:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Water:
                    case OneEventEnumAttribute.Wind:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.WindFire:
                switch (attacker)
                {
                    case OneEventEnumAttribute.Fire:
                    case OneEventEnumAttribute.Wind:
                    case OneEventEnumAttribute.FireWater:
                    case OneEventEnumAttribute.WaterWind:
                    case OneEventEnumAttribute.WindFire:
                    case OneEventEnumAttribute.FireWaterWind:
                        return true;
                }

                break;
            case OneEventEnumAttribute.FireWaterWind:
                return true;
            default:
                Debug.LogError("no attribute like " + attacker.ToString());
                break;
        }

        return false;
    }
}

#今回きれいにしたポイント

  • enumの拡張メソッドとして定義する。
  • 複合属性は、単属性に分けてから計算する。

コードの行数的にも半分以下になったし、なにより、読みやすくなりました。
きもちいぃ!

#今回きれいにしたコード

OneEventEnumAttribute.cs
public enum OneEventEnumAttribute
{
    Dami = 0,
    Fire = 1,
    Water = 2,
    Wind = 3,
    FireWater = 4,
    WaterWind = 5,
    WindFire = 6,
    FireWaterWind = 7,
}

public static partial class OneEventEnumAttributeExtend
{
    public static bool Contains(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        var thisAttributes = param.ConvertSingleAttributeArray();
        var targetAttributes = target.ConvertSingleAttributeArray();

        foreach (var targetAttribute in targetAttributes)
        {
            foreach (var thisAttribute in thisAttributes)
            {
                if (targetAttribute == thisAttribute)
                {
                    return true;
                }
            }
        }

        return false;
    }

    public static float GetDamageRate(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        var thisAttributes = param.ConvertSingleAttributeArray();
        var targetAttributes = target.ConvertSingleAttributeArray();

        int counta = 0;
        float sumRate = 0;
        foreach (var targetAttribute in targetAttributes)
        {
            foreach (var thisAttribute in thisAttributes)
            {
                sumRate += GetSingleDamageRate(thisAttribute, targetAttribute);
                counta++;
            }
        }

        return sumRate / counta;
    }

    private static float GetSingleDamageRate(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        if (param == target)
        {
            return 1f;
        }

        if (param.IsStrongFor(target))
        {
            return 1.5f;
        }

        return 0.5f;
    }

    private static bool IsStrongFor(this OneEventEnumAttribute param, OneEventEnumAttribute target)
    {
        if (param == target)
        {
            return false;
        }

        switch (param)
        {
            case OneEventEnumAttribute.Fire:
                return target == OneEventEnumAttribute.Wind;
            case OneEventEnumAttribute.Water:
                return target == OneEventEnumAttribute.Fire;
            case OneEventEnumAttribute.Wind:
                return target == OneEventEnumAttribute.Water;
            default:
                throw new ArgumentOutOfRangeException(nameof(param), param, null);
        }
    }

    private static OneEventEnumAttribute[] ConvertSingleAttributeArray(this OneEventEnumAttribute param)
    {
        switch (param)
        {
            case OneEventEnumAttribute.Dami:
                return new OneEventEnumAttribute[0];
            case OneEventEnumAttribute.Fire:
                return new OneEventEnumAttribute[1] {OneEventEnumAttribute.Fire};
            case OneEventEnumAttribute.Water:
                return new OneEventEnumAttribute[1] {OneEventEnumAttribute.Water};
            case OneEventEnumAttribute.Wind:
                return new OneEventEnumAttribute[1] {OneEventEnumAttribute.Wind};
            case OneEventEnumAttribute.FireWater:
                return new OneEventEnumAttribute[2] {OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water};
            case OneEventEnumAttribute.WaterWind:
                return new OneEventEnumAttribute[2] {OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind};
            case OneEventEnumAttribute.WindFire:
                return new OneEventEnumAttribute[2] {OneEventEnumAttribute.Wind, OneEventEnumAttribute.Fire};
            case OneEventEnumAttribute.FireWaterWind:
                return new OneEventEnumAttribute[3]
                    {OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind};
            default:
                throw new ArgumentOutOfRangeException(nameof(param), param, null);
        }
    }
}

#参考:修正したコードを検証するテストコード(NUnit)

OneEventEnumAttributeTest.cs
using NUnit.Framework;


public class OneEventEnumAttributeTest
{
    [Test]
    public void TestGetDamageRate()
    {
        OneEventEnumAttribute[] attackerAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };
        OneEventEnumAttribute[] targetAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };

        foreach (var attackerAttribute in attackerAttributes)
        {
            foreach (var targetAttribute in targetAttributes)
            {
                var expectedResult = new OneEventAttributeDamageLogic().Caluculate(attackerAttribute, targetAttribute);
                var damageRate = attackerAttribute.GetDamageRate(targetAttribute);
                Assert.AreEqual(expectedResult, damageRate, "from:" + attackerAttribute + " to:" + targetAttribute);
            }
        }
    }

    [Test]
    public void TestContains()
    {
        OneEventEnumAttribute[] attackerAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };
        OneEventEnumAttribute[] targetAttributes = new OneEventEnumAttribute[7]
        {
            OneEventEnumAttribute.Fire, OneEventEnumAttribute.Water, OneEventEnumAttribute.Wind,
            OneEventEnumAttribute.FireWater, OneEventEnumAttribute.WaterWind, OneEventEnumAttribute.WindFire,
            OneEventEnumAttribute.FireWaterWind
        };

        foreach (var attackerAttribute in attackerAttributes)
        {
            foreach (var targetAttribute in targetAttributes)
            {
                var expectedResult = new OneEventAttributeLogic().CanAttack(attackerAttribute, targetAttribute);
                var nowResult = attackerAttribute.Contains(targetAttribute);
                Assert.AreEqual(expectedResult, nowResult, "from:" + attackerAttribute + " to:" + targetAttribute);
            }
        }
    }
}

#おわりに
開発者のツイッターはこちら
プログラマさん。ゲーム開発者さん。仲良くしたいです。

英語学習ゲーム 英語物語で遊んでみてね♪

713ikWQGVJL.png

3
3
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?