55

I'm currently working on adding sound effects to a game, and although my current code is working fine, I'm looking for a way to simplify it. Basically, each object in the game has a string value indicating its material (ie. "wood", "metal", etc.), and when two objects collide, a sound effect is played based on the combination. The code essentially looks like this:

if( (matA == "metal" && matB == "wood") || (matA == "wood" && matB == "metal") )
{
    //play sound for metal-wood collision
}

But I'm wondering if there's a way to simplify the if-statement to something like this:

if( one of the materials is wood && one of the materials is metal )
{
    //play sound for metal-wood collision
}
Coffee Boat
  • 749
  • 6
  • 16
  • 6
    Is the simplification necessary for reasons of performance or just "nicer looking code"? If the the first is true, you could assign each material a prime number instead of a string (maybe using macros/enums) and check for the product of two colliding materials. That way you have a unique number for each combination of two materials. – philkark Apr 17 '16 at 10:01
  • 1
    You _could_ do something like `if (new HashSet { matA, matB, }.SetEquals(new HashSet { "metal", "wood", }))` but it will run slower than what you have. You actually only need `.IsSupersetOf` but that might be less obvious for readers of the code, and I think `HashSet<>` has optimizations which will mean that you do not benefit much in performance from using `.IsSupersetOf`. In any case, if you had many more than two variable, this would start to make more sense. With two variables, stick with what you have, or consider nice syntax from weston's answer. – Jeppe Stig Nielsen Apr 17 '16 at 10:22
  • 4
    @phil13131 did you just reccomend a prime factorization to identify unique flags? That's a delightful creative solution, but assigning them each a power of 2, and just using the standard but manipulation methods would certainly be more intuitive and idiomatic – Steve Cox Apr 17 '16 at 14:49
  • @phil13131 The simplification is for nicer looking code. I felt like there had to be a cleaner approach to check for combinations of materials without worrying about the order of appearance or having a wall of if-else statements that's prone to a typo or forgotten combination. – Coffee Boat Apr 17 '16 at 18:01
  • @SteveCox I agree, however, I proposed this as this would only leave you with 32 possible materials for 4-byte integers and I would imagine that a situation occurs where more is necessary (even more than 64 for longs). But of course bit maps are an even faster and cleaner solution if possible. – philkark Apr 17 '16 at 20:09
  • @phil13131 But bit arithmetic is faster (if it's optimized - multiplication is actually a series of bitshifts) and optimal as well, which is why almost everyone uses it. This way, you could have a 16-bit space of materials (~65k I think), and just have materials in an enum and use `public static int operator +(Material m1, Material m2) => m1 > m2 ? m2 << 16 + m1 : m1 << 16 + m2` and `public static implicit operator int(Material m1) => (int) m1._material` in a user-defined class. – somebody Apr 18 '16 at 08:26

5 Answers5

56

You should use an enum for materials instead of string and you can use a Dictionary to hold corresponding sound combinations. You can skip multiple if statements and select corresponding object for each material automatically using the Dictionary. For example:

[Flags]
enum Material  
    {  
        Wood=1,
        Iron=2,
        Glass=4
        //...  
    }  
Dictionary<Material,SoundObject> sounds = new Dictionary<Material,SoundObject>();  
sounds.add(Material.Wood,woodSound);  
sounds.add(Material.Iron,ironSound);  
sounds.add(Material.Wood | Material.Iron,woodAndIronSound);

// And play corresponding sound directly without any if statement.  
sounds[object.Material].Play();  
sounds[matA | matB].Play();  

Performance advantages:

You also will improve performance by using this approach. because definitely integer comparison of Enum values or hash codes would easier and faster than string comparison. And about dictionary VS multiple if-else statements, series of if/else if statements executes linearly; so its performance very depends on number of if statements and equality comparer of object; while Dictionary is based on a Hashtable. It uses an index-optimized collection to store values, which has effectively constant access time. It means often there is no matter how many keys are in dictionary, you will access to values in a constant time and in most scenarios it's very faster than multiple if statements.

Performance comparison:

We will compare performance of two approach in this example:

//If you want to try, just copy the code and see the result.  
static Dictionary<char, short> myHashTable = Enumerable.Range((short)'A', (short)'z').ToDictionary((ch) => (char)ch, (sh) => (short)sh);  

static void Main(string[] args)  
{  
    System.Diagnostics.Stopwatch SW = new   System.Diagnostics.Stopwatch();  
    short temp = 0;  
    SW.Start();  
    for(int i=0;i<10000000;i++)  
    temp = getValue('z');  
    SW.Stop();  
    Console.WriteLine(SW.ElapsedMilliseconds );  
    SW.Reset();              
    SW.Start();  
    for(int i =0;i<10000000;i++)  
    temp = myHashTable['a'];  
    SW.Stop();  
    Console.WriteLine(SW.ElapsedMilliseconds);  
}  
static short getValue(char input)  
{  
    if (input == 'a')  
        return (short)'a';  
    else if (input == 'b')  
        return (short)'b';  
    else if (input == 'c')  
        return (short)'c';  
    else if (input == 'd')  
        return (short)'d';  
    else if (input == 'e')  
        return (short)'e';  
    else if (input == 'f')  
        return (short)'f';  
    else if (input == 'g')  
        return (short)'g';  
    else if (input == 'h')  
        return (short)'h';  
    else if (input == 'i')  
        return (short)'i';  
    else if (input == 'j')  
        return (short)'j';  
    else if (input == 'k')  
        return (short)'k';  
    else if (input == 'l')  
        return (short)'l';  
    else if (input == 'm')  
        return (short)'m';  
    else if (input == 'n')  
        return (short)'n';  
    else if (input == 'o')  
        return (short)'o';  
    else if (input == 'p')  
        return (short)'p';  
    else if (input == 'q')  
        return (short)'q';  
    else if (input == 'r')  
        return (short)'r';  
    else if (input == 's')  
        return (short)'s';  
    else if (input == 't')  
        return (short)'t';  
    else if (input == 'u')  
        return (short)'u';  
    else if (input == 'v')  
        return (short)'v';  
    else if (input == 'w')  
        return (short)'w';  
    else if (input == 'x')  
        return (short)'x';  
    else if (input == 'y')  
        return (short)'y';  
    else if (input == 'z')  
        return (short)'z';  
    return 0;  

} 

result:

if statements with 26 items| dictionary with 122 items.
593 254
579 256
572 252
570 246
587 248
574 291
576 246
685 265
599 282
723 338

which indicates dictionary is more than 2 times faster than if/else if statements.

Smittey
  • 2,475
  • 10
  • 28
  • 35
mehrdad safa
  • 1,081
  • 9
  • 10
  • 3
    This is a great idea, just need to make it clear that the dictionary must be set up once and reused lots. As it is, it looks like the OP would do that just before checking the dictionary. – weston Apr 17 '16 at 12:16
  • 9
    I would like to point out the [[Flags](http://stackoverflow.com/questions/8447/what-does-the-flags-enum-attribute-mean-in-c)] Enum attribute which is designed for this kind of use – CAD97 Apr 17 '16 at 17:34
  • Avoiding a wall of if-else-statements altogether sounds wonderful, so I'll definitely give this a try. Thank you :) – Coffee Boat Apr 17 '16 at 18:08
  • 9
    Pointing out that if you're going to do things like `Material.Wood | Material.Iron`, the enum values should be powers of 2: `Wood = 1, Iron = 2, Wool = 4, Glass = 8, ...` – luqui Apr 17 '16 at 18:55
  • @weston Thank you for your attention and notice. I edited the post, may you visit again and give your idea please. – mehrdad safa Apr 18 '16 at 21:17
  • @weston Thanks for your attention. – mehrdad safa Apr 18 '16 at 21:19
  • @Rimply I glad I could help. – mehrdad safa Apr 18 '16 at 21:21
13

The normal approach when you find yourself repeating code is to extract a method:

if (IsWoodAndMetal(matA, matB) || IsWoodAndMetal(matB, matA))
{
    // play sound for metal-wood collision
}

Where IsWoodAndMetal is defined as:

public static bool IsWoodAndMetal(string matA, string matB)
{
    return matA == "wood" && matB == "metal";
}

This will be as fast as the original code unlike all the linq/list and string concatenation solutions that allocate memory which is bad news for a frequent game loop as it causes more frequent and/or longer garbage collections.

We can go further if the || still bothers you, extracting:

public static bool EitherParameterOrder<T>(Func<T, T, bool> func, T a, T b)
{
    return func(a, b) || func(b, a);
}

It now reads:

if (EitherParameterOrder(IsWoodAndMetal, matA, matB))
{
    // play sound for metal-wood collision
}

And I'd still fancy the performance of that over the other solutions (apart from the dictionary solution when you've got a few entries).

weston
  • 54,145
  • 21
  • 145
  • 203
9

It might not be the most modern solution, but using prime numbers as references to your materials could increase your performance. I know and understand that "optimizing before it's necessary" is what many programmers don't recommend, however, in this context I think it does by far not increase the complexity of the code, but increases the performance of this (fairly trivial) task.

public static class Materials
{
   public static uint Wood = 2,
   public static uint Metal = 3,
   public static uint Dirt = 5,
   // etc...
}

if(matA*matB == Materials.Wood*Materials.Metal)
{
   //play sound for metal-wood collision
}

//or with enums but annoying casts are necessary...

enum Materials:uint
{
   Wood = 2,
   Metal = 3,
   Dirt = 5,
   // etc...
}

if((uint)matA*(uint)matB == (uint)Materials.Wood*(uint)Materials.Metal)
{
   //play sound for metal-wood collision
}

This approach is independent of order of materials (commutative multiplication) and does not need any long comparison of strings or any more complex structures than integers.

Assuming that you would like to keep all reference numbers 4-byte integers and the square root of the largest 4-byte integer is around 65535, that would leave you with around 6550 possible prime numbers below 65535, such that no product would cause an integer overflow. That should by far be enough for any common game.

philkark
  • 2,417
  • 7
  • 38
  • 59
  • 7
    This is simply a more convoluted version of or'ing binary flags as given in another [answer](http://stackoverflow.com/a/36674826/360211). Much easier to `|` for machine, easier to follow what's going on without need for explanatory comments, and no need to work out primes. – weston Apr 17 '16 at 11:57
  • @weston but in that answer one has to add all combinations between any two materials and that in turn increases the search time through the dictionary. – philkark Apr 17 '16 at 12:07
  • Yes, but I meant ignoring the dictionary part. i.e. you could still do: `if (matA | matB == Material.Wood | Material.Iron)` And have a simpler but equivalent solution to yours. – weston Apr 17 '16 at 12:09
  • @weston But that might cause overlaps between materials if you don't take care of it properly. The reason why I use primes is that any product is unique, whereas using `|` might cause possible overlaps between different materials. – philkark Apr 17 '16 at 12:12
  • 1
    No. Everyone who programs knows the sequence, 1,2,4,8,16, 32... for binary flags. No one knows or uses the sequence for primes. – weston Apr 17 '16 at 12:14
  • 3
    True, but that only leaves you with 32 possible materials using 4-byte integers. What if he needs more? I agree if he only needs such a low trivial amount, it is better, but if he doesn't want to take care of scaling, I would still prefer my solution. – philkark Apr 17 '16 at 12:15
  • Well yeah that's a great point, I can see the merit in this now. – weston Apr 17 '16 at 12:19
  • Note that the comment about the dictionary being slow; dictionaries are extremely efficient when given a good hash function, O(1). http://www.dotnetperls.com/dictionary-time I.e. the number of entries in them doesn't affect the look up time. – weston Apr 17 '16 at 12:22
  • Only being left with 32 possible materials is possibly a misleading argument here. If we use primes, how many combinations of 2 materials can by multiplied without overflowing? And what if we want to start combining more than one material? How many combinations of 3, 4, 5, etc., can we multiply without overflowing? With binary `|`-ing, we can have 32 materials, but we can have *any* combination of *any* number of these materials. – nhgrif Apr 17 '16 at 12:39
  • 1
    @nhgrif If you had fully read my answer, you would know that I have provided that information. You can use around 6550 materials using 4-byte integers. Has has expressed that he wants "colliding" materials, so it is fair to assume he only means combination of 2. – philkark Apr 17 '16 at 12:58
  • For combinations of two materials. How about for combinations of 32 materials? – nhgrif Apr 17 '16 at 12:59
  • 1
    @nhgrif That is currently outside the scope of this question. First you'd have to define what a multiple-material collision is supposed to look like, and why it should sound different from three pairs of independent collisions. Anyway, two materials only is a safe assumption based on the question and this is a decent, if a bit obscure, solution for it. Should be profiled/benchmarked against using a dictionary and the faster one picked if performance is critical - which it might be, for a game/physics engine. – Bob Apr 17 '16 at 15:48
  • Why not using a `switch` statement instead of the `if` cascade? – A.H. Apr 19 '16 at 05:52
7

You should change the mat{A,B} type to enum. Which would be defined as follows:

[Flags]
enum Materials {
    Wood = 1,
    Metal = 2,
    Concrete = 4,
    // etc ...
}

Then the code would look like this:

Meterials matA = Materials.Wood;
Meterials matB = Materials.Metal;

if ((matA | matB) == (Materials.Metal | Materials.Wood))
{
    // Do something
}

The only problem here is, that matA, can now be of type Wood and Metal at the same time, but this problem was also present in the string solution.

--- EDIT ---

It is also possible to create the enum alias for wood and metal

[Flags]
enum Materials
{
    Wood = 1,
    Metal = 2,
    WoodMetal = Wood | Metal,
    Concrete = 4,
    // etc
}

Then the code would look like this:

Materials matA = Materials.Wood;
Materials matB = Materials.Metal;

if ((matA | matB) == Materials.WoodMetal)
{
    // Do something
}
  • `WoodMetal` can be defined as `WoodMetal = Wood ^ Metal` or `WoodMetal = Wood | Metal` (as I would prefer) – weston Apr 18 '16 at 07:59
  • Your choice of `^` over `|` is not [standard for Flags](https://msdn.microsoft.com/en-us/library/system.flagsattribute(v=vs.110).aspx). It could also cause problems e.g: `WoodMetal ^ Wood == Metal`, and `Metal`, `WoodMetal | Wood == WoodMetal` the former is fine if that's what you want to do, but when we want to combine flags, not switch them off, `|` makes more sense. – weston Apr 18 '16 at 08:03
  • @weston thank you for the comment, I fixed the code. –  Apr 18 '16 at 09:12
  • OK, but you missed the first point. The line `WoodMetal = 3, // alias for Wood | Metal` can be: `WoodMetal = Wood | Metal,` – weston Apr 18 '16 at 09:23
6

I feel compelled to post what I'd consider the most 'obvious' solution, which nobody else seems to have posted yet. If the accepted answer works for you, go with that one. I'm just adding this for completeness.

First, define a static helper method that does the comparison both ways:

public static bool MaterialsMatch(string candidate1, string candidate2, 
                                   string expected1, string expected2)
{
    return (candidate1 == expected1 && candidate2 == expected2) || 
           (candidate1 == expected2 && candidate2 == expected1);
}

Then use that in your if statements:

if (MaterialsMatch(matA, matB, "wood", "metal"))
{
    // play sound for wood-metal collision
}
else if (MaterialsMatch(matA, matB, "wood", "rubber"))
{
    // play sound for wood-rubber collision
}
else if (MaterialsMatch(matA, matB, "metal", "rubber"))
{
    // play sound for metal-rubber collision
}
// etc.
BenM
  • 459
  • 3
  • 7