Is it possible to simplify an if-statement that checks for a combination?
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
}
Solution 1:
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.
Solution 2:
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).
Solution 3:
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.
Solution 4:
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
}
Solution 5:
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.