I am trying to build a simple video game engine using C#. This is a long time project, which I only want to do out of interest. For the beginning I wanted to implement a math library for linear algebra. I am just a hobby programmer and I am unsure how to make my code as maintainable as possible in the long term. The following questions represent only examples of a problem, which I assume to have a lot more often in this project. I ask this question with the hope to structure my project properly from the beginning to keep it easy to test and to maintain.
What is the best way to define repeating methods like for example the dot product for vector classes of different dimensions?
The first implementation I made was to hard code the methods for all vector classes:
class Vector2 {
public float X { get; }
public float Y { get; }
// Operations
public static float Dot(Vector2 a, Vector2 b) => a.X * b.X a.Y * b.Y;
...
}
class Vector3 {
public float X { get; }
public float Y { get; }
public float Z { get; }
// Operations
public static float Dot(Vector3 a, Vector3 b) => a.X * b.X a.Y * b.Y a.Z * b.Z;
...
}
The code is simple, but it doesn't take advantage of the pattern behind the dot product. I would have to do this for all the other vector operations (addition, scalar multiplication, etc.). It is redundant and opens room for errors.
An alternative approach would be to define the vector operations once in a base class and to put the vector values in an indexer. In this approach, I would only need to define the abstract method for the indexer in the subclasses:
class abstract Vector {
// Indexer
public float this[int index] values { get => GetValueByIndex(index) };
protected abstract float GetValueByIndex(int index);
// Operations
public static float Dot(Vector a, Vector b) {
float DotProduct = 0;
for (int i = 0; i < values.Length; i ){
DotProduct = a[i] * b[i];
}
return DotProduct;
}
...
}
class Vector2 : Vector {
public float X { get; }
public float Y { get; }
protected override float GetValueByIndex(int index) {
if (index == 0) return X;
if (index == 1) return Y;
throw new ArgumentOutOfRangeException("index must be 0 or 1");
}
}
class Vector3 : Vector {
public float X { get; }
public float Y { get; }
public float Z { get; }
protected override float GetValueByIndex(int index) {
if (index == 0) return X;
if (index == 1) return Y;
if (index == 2) return Z;
throw new ArgumentOutOfRangeException("index must be 0, 1 or 2");
}
}
The hard coding approach should run faster, which is good considering it should be used for a game engine, but it is also opens more room for error. The base class approach is easier to maintain, but the for loops and conditions would make all calculations slower.
Which approach would you prefer or is there an alternative, which allows to get fast and maintainable code at the same time?
CodePudding user response:
Maybe it is better to store your values in an array instead of in the X, Y, Z properties. The properties should get the values from the array. By this, if you access your values by index, you don't need any if blocks. You'll have to do performance measurments in order to find out whether the loop is a problem for you. Look at this example:
public abstract class Vector {
protected abstract int Dimension {get;}
protected float[] Values {get; private set; }
public Vector()
{
Values = new float[Dimension];
}
// Operations
// use generics in order to make sure one cannot multiply a vector2 with a vector3
public static float Dot<TVector>(TVector a, TVector b) where TVector : Vector {
float DotProduct = 0;
for (int i = 0; i < a.Values.Length; i ){
DotProduct = a.Values[i] * b.Values[i];
}
return DotProduct;
}
}
public class Vector2 : Vector
{
protected override int Dimension => 2;
public float X => Values[0];
public float Y => Values[1];
}
public class Vector3 : Vector
{
protected override int Dimension => 3;
public float X => Values[0];
public float Y => Values[1];
public float Z => Values[2];
}
CodePudding user response:
If the context is a video game you should probably define separate types for 2, 3 and 4 dimensions. These are the only sizes used in practice for computergraphics. See for example System.Numerics and Math.Net.Spatial. You might also consider just using one of these rather than writing your own.
If your intended use is things like math and optimizations, then you might work on vectors that have thousands of elements, and then you need some type of variable sized storage, probably an array. See Math.Net again.
Since you want your dot products to be fast, the best option is to use your first example, this will be much easier for the compiler and processor to optimize than any loops. For simple code like a dot product any errors should be apparent if you just look at the code with a critical eye. For anything even slightly complicated you should write unit tests. However, if you use an existing library it is likely already well tested.
It is also fairly common to need a vectors of a few different types, like int, float, double. The common approach have been to just make one type for each, Vector2f
, Vector2i
etc. But this might change with the introduction of Generic Math, and since you would use generics, the compiler should be able to optimize everything well. You can also apply the same interfaces to your own types, and you may be able to write generic implementations of some algorithms.
Note that you almost certainly want to use structs for your types, using classes will need additional memory for references and object headers. You probably also want to make your structs readonly and use in parameters for larger types to avoid copy-overhead. You should also be careful when using interfaces since these can cause boxing. Use generic constraints to avoid the overhead, or only use interfaces where the performance is not relevant.
One curious observation is that roughly half of the libraries use static methods (see System.Numerics), and half use member methods (Math.Net). I have never quite understood why, or if there is any objective advantage to one or the other.
Vector code can also be optimized by using SIMD to process all elements in a vector with a single instruction. System.Numerics should already support this on some platforms.