Bearing in mind that all these functions take a single argument and return asingle value, there isn’t really a problem with having a procedural interface:// File: MATHS_TranscendentalFun
Trang 1Bearing in mind that all these functions take a single argument and return asingle value, there isn’t really a problem with having a procedural interface:// File: MATHS_TranscendentalFunctions.hpp
#include <cmath>
namespace MATHS{
template<class T>
inline T Sin( T x ){
return( (T)::sinf(float(x));
}
// and so on…
}Implementing our own transcendentals lets us control how the functions areevaluated For example, the trig functions can be implemented by table look-up(though the days when that was a requirement are fast receding)
Now it’s time to consider the intermediate-level maths components.Interestingly, they are generalisations of low-level components, adding the abil-ity to change size dynamically to those classes This is accompanied by a growth
in the computational cost of using these classes
The MATHS::Vectorclass is a general-purpose mathematical vector It isresizable like an array class, but it supports all the arithmetical and mathemati-cal operations of the low-level vector classes (except the cross-product)
// File: MATHS_Vector.hppnamespace MATHS
{
template<class T>
class Vector{
public:
// Lifecycle
Vector( int Size );
Vector( const Vector<T> & rVector );
~Vector();
Trang 2// Access.
int GetSize() const { return m_iSize; }void SetSize( int iNewSize );
// Operators
Vector<T> & operator=( const Vector<T> & rVector );
T operator[]( int n ) const;
T & operator[]( int n );
Vector<T> & operator+=( const Vector<T> & rVector );
Vector<T> & operator-=( const Vector<T> & rVector );
Vector<T> & operator*=( T s );
friend T operator*(const Vector<T>&v1,
const Vector<T> &v2);
friend Vector<T> operator+( const Vector<T> & v1,
const Vector<T> & v2 );
friend Vector<T> operator-(const Vector<T> & v1,
const Vector<T> & v2);
friend Vector<T> operator*(T s,const Vector<T> & v);
friend Vector<T> operator*(const Vector<T>& v, T s);
friend bool operator==( const Vector<T> & v1,
const Vector<T> & v2 );
const Vector<T> operator-();
// Other processing
T Length() const { return Sqrt( LengthSquared() ); }
T LengthSquared() const { return((*this)*(*this)); }void Normalise();
Vector<T> Normalised() const;
void Fill( T tValue );
Structurally, there’s very little new here However, its relation, the arbitrarily sized
matrix, is quite a bit more complicated than its low-level cousins The
complex-ity comes about because simple matrix operations can cause quite a number of
dynamic memory operations Consider, for example, the Transpose()method
This turns an n × m matrix into an m × n matrix (see Figure 5.10).
Trang 3Unless the matrix is square – n × n – there’s going to be quite a bit of shuffling
around of data in memory to effect that transposition In order to make this a biteasier, we create an auxiliary class called a Buffer, which is private to the matrix.Buffers (and hence matrices) can be constructed from either a new block ofdynamic memory or a preallocated static memory block The latter allows us oncertain target platforms to point the matrix at areas of fast (uncached) memory,and in general it can avoid the multiple calls to dynamic allocations and dele-tions that can occur during seemingly innocent matrix operations
// File: MATHS_Matrix.hppnamespace MATHS
{
template<class T>
class Matrix{
public:
// Error codesenum Error{
ERR_NONE,ERR_OUT_OF_MEMORY,ERR_SINGULAR};
// Lifecycle
Matrix();
Matrix( int iRows, int iCols );
Matrix( const Matrix<T> & m );
Matrix( int iRows, int iCols, void * pBuffer );
// Construct a matrix from a static buffer
// Note that the buffer can be single or // multi-dimensional, ie float aBuffer[n*m]
// or float aBuffer[n][m] will work equally
Transpose
Figure 5.10Transposing a matrix
changes its shape
Trang 4Matrix<T> Transposed() const;
int Rank() const;
int Rows() const;
int Columns() const;
int Capacity() const;
Matrix<T> GetSubMatrix( int iStartCol,
int iStartRow, int iRows, int iCols ) const;
bool IsSquare() const;
// Unary/class operators
Matrix<T> & operator-();
T operator()( int iRow, int iCol ) const;
T & operator()( int iRow, int iCol );
Matrix<T> & operator=( const Matrix<T> & m );
Matrix<T> & operator+=( const Matrix<T> & m );
Matrix<T> & operator-=( const Matrix<T> & m );
Matrix<T> & operator*=( T s );
Matrix<T> operator+( const Matrix<T> & m ) const;
Matrix<T> operator-( const Matrix<T> & m ) const;
// Binary operators
friend Matrix<T> operator*( const Matrix & m1,
const Matrix & m2 );
friend Matrix<T> operator*(T s,const Matrix<T>&m1);
friend Matrix<T> operator*(const Matrix<T>&m1,T s);
// In-place operations
static void InPlaceTranspose( Matrix<T> & rTrans,
const Matrix<T> & m );
static void InPlaceMultiply( Matrix<T> & rProduct,
const Matrix<T> & m1,
Trang 5class Buffer{
Buffer(int iRows, int iColumns);
Buffer(int iRows, int iColumns, void *pBuffer);
~Buffer();
T GetAt( int iRow, int iCol ) const;
T & GetReference( int iRow, int iCol );
void SetAt( int iRow, int iCol, T tValue );
inline T * GetDataPtr() const;
inline bool IsStatic() const;
in the linear algebra subcomponent For example, here’s the function that gets
the nth column of a matrix:
// File: MATHS_LinearAlgebra.hpp//…
#include "MATHS_Matrix.hpp"
#include "MATHS_Vector.hpp"
//…
namespace MATHS{
template<class T>
Vector<T> GetColumn( Matrix<T> const & m );
// etc
Trang 6What’s that you say? A C-style (free) function? Isn’t that not at all
object-ori-ented? Worry not Remember that a class member function is really a C
function that gets a hidden ‘this’ pointer Besides, consider what one objective
of encapsulation is – to protect client code from internal changes to a class A
possible way of measuring encapsulation loss, therefore, is to consider how
many files need to be recompiled as the result of the addition or removal of a
method If we added GetColumn()as a member function, then all the client
files that included MATHS_Matrix.hpp would rebuild By adding it as a free
function in another file, we have preserved encapsulation
Finally, it’s time to look at the high-level services offered by the maths
compo-nent The first – and most straightforward – class to consider is the complex
number class Now, it’s going to be pretty unusual to find complex numbers in
game code However, when we write tools that perform intricate offline
graphi-cal graphi-calculations, it is not beyond the bounds of possibility that complex
solutions to equations may arise, so a complex number class is useful to have
ready to hand
In this circumstance, the STL version, template<class T> std::complex<T>
will suffice, because although it’s pretty top-heavy it’s only really used in code
that is not time-critical
The next – and altogether more important – class (actually set of classes) isthe interpolators In non-technical terms, an interpolator is a mathematical
function (here, restricted to a single real parameter) that returns a real value
based on a simple relationship over an interval For example, a constant
inter-polator returns the same value (said constant) whatever the argument (see
Figure 5.11a) A linear interpolator returns a value based on a linear scale (see
Figure 5.11Constant and linearinterpolants
Trang 7template<class T>
class Interpolator{
public:
virtual T Interpolate( T x ) = 0;
};
}And we extend the behaviour in the expected fashion:
// File: MATHS_InterpolatorConstant.hpp
#include "MATHS_Interpolator.h"
namespace MATHS{
template<class T>
class InterpolatorConstant : public Interpolator<T>
{private:
T m_C;
public:
InterpolatorConstant( T c ): m_C(c)
{}
T Interpolate( T x ){
return m_C;
}};
}This defines a series of classes, each using a higher-degree polynomial to com-pute the return value, none of which is particularly powerful on its own.However, consider the graph shown in Figure 5.12 A relationship like thismight exist in a driving game’s vehicle model between engine rpm and theavailable torque
Of course, this is a greatly simplified model, but it has most of the istics of the real thing The range of the rpm parameter will be somethingbetween zero and 10 000 Now, we could encode this graph as a look-up table Ifthere’s one 32-bit float per entry, that’s nearly 40KB for the table, which is quite a
Trang 8character-lot considering how simple the relationship looks It’s what’s known as a
‘piece-wise relationship’, the first piece being increasingly linear, the second constant,
and the third decreasingly linear So let’s design a piecewise interpolator object
After many hours in my laboratory, the results are as shown in Figure 5.13
The piecewise interpolator contains a series of (non-overlapping) ranges,each of which has its own interpolator (which can itself be piecewise) The code
looks like this:
InterpolatorLinear
Interpolator
InterpolatorConstant InterpolatorPiecewise
Range
T
Maximum Minimum
*Ranges
Object diagram for theinterpolator component
Trang 9T Interpolate( T x ){
// Find interval containing x
for( int j = 0; j < m_Ranges.size(); ++j ){
Range & r = m_Ranges[j];
if ( x >= r.tMin && x < r.tMax ){
return(r.pInterp->Interpolate(x));
}}
// Return 0 for illustration – you should add an// "otherwise return this" value
Suppose there’s a projectile with position (vector) x and velocity vector v.
Then, after a time interval t the new position is given by
x(t) = x + t*v
However, this works only if v is constant during the time interval If v varies,
then the calculated position will be wrong To get better approximations to the
new position, we can split t into successively smaller slices of time, calculate the
position at the end of each of these subintervals, and accumulate the resultingchange And that process is the job of an integrator Since games are frequentlyconcerned with objects, velocities and positions, you might appreciate why theyhave their uses
Integration is not cheap The more you subdivide the interval, the morework gets done, though you get more accurate results For basic projectilemotion, objects falling under gravity or other simple cases, an integrator would
be overkill However, for some simulation purposes, they are essential So let’sfocus our OO skills on designing integrator classes
Trang 10The first thing to realise is that there are many ways to perform the tion Each method comes with an associated error term and stability A stable
integra-integrator has the property that the error term stays small no matter how big
the interval t is As you can imagine, very stable integrators are gratuitously
expensive in CPU cycles However, it’s possible to write a moderately stable
inte-grator (which will work nicely for small time intervals) that is reasonably
accurate So we’re looking at a family of integrator classes based on an abstract
base class
However, there’s a twist Integration is something that is done to an object,
so consider the following method declaration:
void Integrate( /*???*/ & aClient );
Exactly what type do we pass in? The integrator needs to be able to get at client
data, so we should think about providing an abstract mix-in property class with
an abstract interface that allows just that As a first step on this path, consider
the following class:
virtual void GetArgument( VectorNf & v ) = 0;
virtual void GetValue( VectorNf & v ) = 0;
virtual void SetArgument( VectorNf const &v ) = 0;
virtual int GetDimension() const = 0;
};
}
This class abstractly represents a vector equation
y = f(x) where f is a function that takes a vector x as an argument and returns a vector y
Trang 11With the aid of this class, we can create a subclass that confers the property
of being integrable:
// File: MATHS_Integrator.hpp
#include "MATHS_VectorFunction.hpp"
namespace MATHS{
class IsIntegrable : public VectorFunction{
public:
IsIntegrable () : VectorFunction() { ; }
void GetStateVector( VectorNf & vLhs );
void GetDerivativeVector( VectorNf & vLhs );
void SetStateVector( VectorNf & vResult );
};
inline void IsIntegrable::GetStateVector( VectorNf & vLhs ){
GetArgumentVector( vLhs );
}
// -inlinevoid IsIntegrable::GetDerivativeVector( VectorNf & vRhs ){
GetFunctionVector( vRhs );
}
// -inline void IsIntegrable::SetStateVector( VectorNf & rResult ){
SetArgumentVector( rResultVector );
}
}
Trang 12Now we can write our abstract integrator class:
int GetNumberOfUnknowns() const;
void SetStepGranularity( float Step );
float GetStepGranularity() const;
float GetIntegrationTime() const;
virtual void SetNumberOfUnknowns( int n );
// The integrator
virtual bool Integrate(IsIntegrable*,float h);
virtual bool Step(IsIntegrable*, float h ) = 0;
float m_CurrentTime;
};
Trang 13All of the hard work is done in the subclass implementation of the virtual tion Step( IsIntegrable * pClient, float h ) All of the data that theintegrator needs are extracted via the IsIntegrableinterface so there is noneed to violate encapsulation at any level The only slight concern we mighthave is the protected status of the Input, Result and Rhs vectors within the inte-grator base class In-line set and get accessors can quell your fears The overallstructure is summed up in Figure 5.14.
func-The IntegratorEulerand IntegratorMidpointsubclasses express twosimple ways of performing the integration (the details of which are unimpor-tant here) Other methods exist, but an object that has a reference to anintegrator need not care about exactly what mathematics are going on insidethe Step()method
5.4.5 Text and language processingMost games process text in some way Although it is bulky and slow to turn into
a useful internal format, it is very handy early on in the product lifecycle to beable to read text files with an eye to reading their binary equivalents later on indevelopment With this in mind, we define an abstract stream class whose sub-classes can be either ASCII streams or binary stream readers – a client who has apointer to a stream neither knows nor cares (see Figure 5.15)
The interface class will look something like this:
// File: STRM_ReadStream.hppnamespace STRM
Figure 5.15Object diagram showing
the stream component
Trang 14virtual bool Open( const char * pFilePath ) = 0;
virtual void Close() = 0;
virtual int ReadToken() = 0;
virtual int ReadByte() = 0;
virtual int ReadShort() = 0;
virtual int ReadInt() = 0;
virtual float ReadFloat() = 0;
virtual int ReadString( char * pBuffer,
int iBufferSize ) = 0;
virtual bool EndOfStream() const = 0;
virtual int GetLength() const = 0;
virtual void Reset() = 0;
virtual Position GetPosition() const = 0;
virtual void SetPosition(const Position &aPos)=0;
Trang 15typedef CONT::hash_table<int,CONT::string> tTokenMap;
One of the big shocks for new developers is the complexities and subtleties ofwriting software that needs foreign language support The problems are, in real-ity, not difficult to solve, but they will cause difficulties if you attempt toretro-fit them into an existing game structure The good news is that by andlarge, linguistic support is not difficult to add A string table class is just aboutall that’s required, mapping an integer to a text string (see Figure 5.16)
String Table
Hello, World\0Hey Mr Tallyman tally
me banana\0I’ve got a lovely bunch of coconuts\0Born under a bad sign with
a blue moon in your eye\0
Text strings
0, 10,
Offset table
enum { HELLO_WORLD, MR_TALLYMAN, COCONUTS,
}
Strings IDs
Text data Offset table
Figure 5.16The constituents of a
string table