Tuesday, October 26, 2010

Generic linked list

Today I will present my generic linked list, the advantage with them is:

  • Insert or delete at any position is linear (compared with arrays that deleting the first element can be painful)
  • Dynamic allocation
some cons:
  • A bit complex to implement
  • No constant time access since the elements are not stored linearly
Having a look at pros/cons I think a good way of using them are FIFO/LIFO queues, let's see the interface (it can be found at BB.Collection.List)




TLinkedList<T> = class(TInterfacedObject, ISorteable, IIterator<T>)
  private
    type
      TNodeList = class
      public
        Item: T;
        Next,
        Prior: TNodeList;
      end;

      TLinkedListEnum = record
      private
        FList: TLinkedList<T>;
        FFirst: boolean;

        function GetCurrent: T;
      public
        constructor Create(aList: TLinkedList<T>);
        function MoveNext: boolean;

        property Current: T read GetCurrent;
      end;

    var
      FIndexed,
      FHead,
      FTail: TNodeList;
      FCount: integer;
      FOwnObjects: boolean;
      FComparer: IComparer<T>;
      FSearchType: TSearchType;

    function GetItem(aIndex: integer): T;
    procedure SetItem(aIndex: integer; const aItem: T);
    procedure CheckIndex(aIndex: Integer);
    procedure TryFreeObject(aItem: T);
    function InternalFind(aItem: T): TNodeList;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Add(aItem: T);
    procedure Insert(aItem, aWhich: T);
    procedure Clear;
    function Count: integer; inline;
    procedure Delete(aItem: T);
    function GetEnumerator: TLinkedListEnum;
    procedure Sort;
    function Find(aItem: T): boolean;
    //IEnumerator
    function GetCurrent: T; inline;
    function First: boolean;
    function Next: boolean;
    function Last: boolean;

    property OwnObjects: boolean read FOwnObjects write FOwnObjects;
    property Items[aIndex: integer]: T read GetItem write SetItem; default; //Slow!!!
    property Comparer: IComparer<T> read FComparer write FComparer;
    property SearchType: TSearchType read FSearchType write FSearchType;
  end;

Nothing special, just maintenance, an enumerator and a property in case T is an object and you need to auto delete the objects and a flag that tells the class is the search starts from the head or from the tail (guess why),

Let's see the implementation:




{ TLinkedList<T> }

procedure TLinkedList<T>.Add(aItem: T);
var
  current: TNodeList;

begin
  //Create new node
  current := TNodeList.Create;
  current.Item := aItem;
  current.Prior := FTail;
  current.Next := nil;

  //Update head
  if FHead = nil then
    FHead := current;

  //Update tail node (previous node)
  if FTail <> nil then
    FTail.Next := current;
  //Tail always point to the last item added
  FTail := current;

  Inc(FCount);
end;

procedure TLinkedList<T>.Clear;
var
  current, tmp: TNodeList;

begin
  current := FHead;
  while current <> nil do
  begin
    TryFreeObject(current.Item);

    tmp := current;
    current := current.Next;
    tmp.Free;
  end;

  FHead := nil;
  FTail := nil;
  FIndexed := nil;
  FCount := 0;
end;

function TLinkedList<T>.Count: integer;
begin
  result := FCount;
end;

constructor TLinkedList<T>.Create;
begin
  FOwnObjects := True;
  FComparer := TComparer<T>.Default;
  FSearchType := stFromFirst;

  Clear;
end;

procedure TLinkedList<T>.Delete(aItem: T);
var
  current, prior, next: TNodeList;

begin
  current := InternalFind(aItem);
  if current = nil then
    raise Exception.Create('Item not found');

  prior := current.Prior;
  next := current.Next;

  TryFreeObject(current.Item);

  //Update head
  if FHead = current then
  begin
    if next <> nil then
      FHead := next
    else
      FHead := nil;
  end;

  //Update tail
  if FTail = current then
  begin
    if prior <> nil then
      FTail := prior
    else
      FTail := nil;
  end;

  //Update sides
  if prior <> nil then
    prior.Next := next;

  if next <> nil then
    next.Prior := prior;

  current.Free;
  Dec(FCount);
end;

destructor TLinkedList<T>.Destroy;
begin
  Clear;

  inherited;
end;

function TLinkedList<T>.Find(aItem: T): boolean;
begin
  result := InternalFind(aItem) <> nil;
end;

function TLinkedList<T>.First: boolean;
begin
  result := FCount > 0;
  if result then
    FIndexed := FHead;
end;

procedure TLinkedList<T>.TryFreeObject(aItem: T);
var
  v: TValue;

begin
  if FOwnObjects then
  begin
    v := TValue.From<T>(aItem);
    if v.IsObject then
      v.AsObject.Free;
  end;
end;

function TLinkedList<T>.GetCurrent: T;
begin
  result := FIndexed.Item;
end;

function TLinkedList<T>.GetEnumerator: TLinkedListEnum;
begin
  result := TLinkedListEnum.Create(self);
end;

function TLinkedList<T>.GetItem(aIndex: integer): T;
var
  i: integer;
  current: TNodeList;

begin
  CheckIndex(aIndex);

  current := FHead;
  for i := 0 to aIndex - 2 do
    current := current.Next;
  Exit(current.Item);
end;

procedure TLinkedList<T>.Insert(aItem, aWhich: T);
var
  NewNode, CurrentNode, prior, next: TNodeList;

begin
  CurrentNode := InternalFind(aWhich);
  if CurrentNode = nil then
    raise Exception.Create('Cannot insert, item does not exists');

  NewNode := TNodeList.Create;
  NewNode.Item := aItem;

  //Update left side
  prior := CurrentNode.Prior;
  if prior <> nil then
  begin
    prior.Next := NewNode;
    NewNode.Prior := prior;
  end else
    NewNode.Prior := nil;

  //Update sides
  NewNode.Next := CurrentNode;
  CurrentNode.Prior := NewNode;

  //Update head?
  if FHead = CurrentNode then
    FHead := NewNode;
end;

function TLinkedList<T>.InternalFind(aItem: T): TNodeList;
var
  current: TNodeList;
  cmp: IComparer<T>;

begin
  result := nil;
  cmp := TComparer<T>.Default;

  if FSearchType = stFromFirst then
    current := FHead
  else
    current := FTail;

  while current <> nil do
  begin
    if cmp.Compare(current.Item, aItem) = 0 then
    begin
      result := current;
      Break;
    end;

    if FSearchType = stFromFirst then
      current := current.Next
    else
      current := current.Prior;
  end;
end;

function TLinkedList<T>.Last: boolean;
begin
  result := FCount > 0;
  if result then
    FIndexed := FTail;
end;

procedure TLinkedList<T>.CheckIndex(aIndex: Integer);
begin
  if aIndex >= FCount then
    raise Exception.Create('Invalid index');
end;

function TLinkedList<T>.Next: boolean;
begin
  result := FIndexed.Next <> nil;
  if result then
    FIndexed := FIndexed.Next;
end;

procedure TLinkedList<T>.SetItem(aIndex: integer; const aItem: T);
var
  i: integer;
  current: TNodeList;

begin
  CheckIndex(aIndex);

  current := FHead;
  for i := 0 to aIndex - 2 do
    current := current.Next;
  current.Item := aItem;
end;

procedure TLinkedList<T>.Sort;
var
  v: TValue;
  list: TList<T>;
  was: boolean;
  i: integer;

begin
  if not First then
    Exit;

  list := TList<T>.Create;
  try
    repeat
      list.Add(GetCurrent);
    until not Next;

    list.Sort(FComparer);

    was := OwnObjects;
    try
      OwnObjects := False;

      Clear;
      for i := 0 to list.Count - 1 do
        Add(list[i]);
    finally
      OwnObjects := was;
    end;

  finally
    list.Free;
  end;
end;

That's it, now I will post the generic queues.

Friday, October 22, 2010

Matrices, for what?

Another very important type in the system is TMatrix, any entity (2d or 3d) that needs to be rotated, scaled, moved, etc in the system uses this type.

Main methods are:

  • Matrix
    • Prepares the matrix for a given vector
  • Identity
  • Add
    • Sum two matrices
  • Multiply
    • Multiply two matrices
  • Subtract
    • Subtracts two matrices
Implementation


unit BB.Math.Matrix;

interface

uses
  BB.Types;

type
  T4x4 = array[0..2, 0..3] of TFloat;

  PMatrix = ^TMatrix;
  TMatrix = record
  public
    _4x4: T4x4;

    constructor Create(aX, aY, aZ: TFloat); overload;
    //constructor Create(const aVector: TVector); overload;
    class operator Multiply(const aMatrixA, aMatrixB: TMatrix): TMatrix; inline;
    class operator Add(const aMatrixA, aMatrixB: TMatrix): TMatrix; inline;
    class operator Subtract(const aMatrixA, aMatrixB: TMatrix): TMatrix; inline;

    procedure Matrix(aX, aY, aZ: TFloat);
    procedure Identity;
    procedure Clear;
    procedure Transpose(const aMatrix: TMatrix);
  end;

implementation

uses
  BB.Math;

{ TMatrix }

class operator TMatrix.Add(const aMatrixA, aMatrixB: TMatrix): TMatrix;
var
  i, j: integer;

begin
  for j := High(T4x4) DownTo Low(T4x4) do
    for i := High(T4x4) DownTo Low(T4x4) do
      Result._4x4[j, i] := aMatrixA._4x4[i, j] + aMatrixB._4x4[i, j];
end;

constructor TMatrix.Create(aX, aY, aZ: TFloat);
begin
  Matrix(aX, aY, aZ);
end;

procedure TMatrix.Identity;
var
  i, j: integer;

begin
  j := High(T4x4);
  while j >= Low(T4x4) do
  begin
    i := High(T4x4);
    while i >= Low(T4x4) do
    begin
      _4x4[j, i] := Ord(i = j);
      Dec(i);
    end;

    Dec(j);
  end;
end;

procedure TMatrix.Matrix(aX, aY, aZ: TFloat);
var
  xSin, xCos, ySin, yCos, zSin, zCos, sxsz, sxcz, szcx, cxcz: TFloat;

begin
  SinCos(aX * RAD, xSin, xCos);
  SinCos(aY * RAD, ySin, yCos);
  SinCos(aZ * RAD, zSin, zCos);

  sxsz := xSin * zSin;
  sxcz := xSin * zCos;
  szcx := zSin * xCos;
  cxcz := xCos * zCos;

  _4x4[0, 0] := yCos * zCos;
  _4x4[1, 0] := yCos * zSin;
  _4x4[2, 0] := -ySin;

  _4x4[0, 1] := ySin * sxcz - szcx;
  _4x4[1, 1] := ySin * sxsz + cxcz;
  _4x4[2, 1] := xSin * yCos;

  _4x4[0, 2] := ySin * cxcz + sxsz;
  _4x4[1, 2] := ySin * szcx - sxcz;
  _4x4[2, 2] := xCos * yCos;
end;

class operator TMatrix.Multiply(const aMatrixA, aMatrixB: TMatrix): TMatrix;
var
  i, j: integer;

begin
  j := High(T4x4);
  while j >= Low(T4x4) do
  begin
    i := High(T4x4);
    while i >= Low(T4x4) do
    begin
      Result._4x4[i, j] :=  (aMatrixA._4x4[i, 0] * aMatrixB._4x4[0, j]) +
                            (aMatrixA._4x4[i, 1] * aMatrixB._4x4[1, j]) +
                            (aMatrixA._4x4[i, 2] * aMatrixB._4x4[2, j])
                             { + (m1[i, 3] * m2[3, j])};
      Dec(i);
    end;

    Dec(j);
  end;
end;

procedure TMatrix.Clear;
begin
  FillChar(_4x4, SizeOf(_4x4), 0);
end;

class operator TMatrix.Subtract(const aMatrixA, aMatrixB: TMatrix): TMatrix;
var
  i, j: integer;

begin
  for j := High(T4x4) DownTo Low(T4x4) do
    for i := High(T4x4) DownTo Low(T4x4) do
      Result._4x4[j, i] := aMatrixA._4x4[i, j] - aMatrixB._4x4[i, j];
end;

procedure TMatrix.Transpose(const aMatrix: TMatrix);
var
  i, j: integer;

 begin
  for j := High(T4x4) downto Low(T4x4) do
    for i := High(T4x4) downto Low(T4x4) do
      _4x4[j, i] := aMatrix._4x4[i, j];
end;

end.
The way that a sprite (surface) ir rotated is via vectors and matrices:
var
  matrix: TMatrix;
  vector: TVector;
  w, h,
  x, y: integer;
poly: TPolygon;

begin
  matrix := TMatrix.Create(0, 0, -aAngle);
  w := GetWidth(Zoom) div 2;
  h := GetHeight(Zoom) div 2;
  x := FX;
  y := FY;

  //1
  //xxx
  //3xxx2
  vector.Rotate(TVector.Create(-w, -h, 0), matrix);
poly[0] := TVector.Create(x + vector.X, y + vector.Y, FZ);
vector.Rotate(TVector.Create(w, h, 0), matrix);
poly[1] := TVector.Create(x + vector.X, y + vector.Y, FZ);
vector.Rotate(TVector.Create(-w, h, 0), matrix);
poly[2] := TVector.Create(x + vector.X, y + vector.Y, FZ);
DrawPoly(poly);
  //1xxx2
  //  xxx
  //    3
  vector.Rotate(TVector.Create(-w, -h, 0), matrix);
poly[0] := TVector.Create(x + vector.X, y + vector.Y, FZ);
vector.Rotate(TVector.Create(w, -h, 0), matrix);
poly[1] := TVector.Create(x + vector.X, y + vector.Y, FZ);
vector.Rotate(TVector.Create(w, h, 0), matrix);
poly[2] := TVector.Create(x + vector.X, y + vector.Y, FZ);
DrawPoly(poly);
//In this case what is filled is a polygon, //so with the rotation of the two triangles we get a rectangle, //after apply a texture on that rectangle we get our sprite zoomed and/or 
  //rotated
end;

Vectors, every now and then...

The 3d engine heavily uses the TVector type, aside from many operators, it also has a functions like:

  • DotProduct
    • Helps finding the angle between two vectors
  • Distance
    • The distance between two products
  • AngleBetween
    • Returns a number between 0 and 359, which corresponds to the angle
  • Rotate
    • Rotates a given vector through a matrix
  • Normal
    • Calculates a vector perpendicular to the plane (given by three vectors)
  • Length
    • Calculates the length of a vector
  • Normalize
    • Normalizes a vector (the vector is transformed to a magnitude of 1)

unit BB.Math.Vector;

interface

uses
  BB.Types, BB.Math.Matrix;

const
  V_RIGHT = 0;
  V_UP = 1;
  V_PN = 2;

type
  PVector = ^TVector;
  TVector = record
  private
    FX,
    FY,
    FZ,
    FW: TFloat;

    function GetIndex(aIndex: integer): TFloat;
    procedure SetIndex(aIndex: integer; const aValue: TFloat);
    procedure SetX(aValue: TFloat); inline;
    procedure SetY(aValue: TFloat); inline;
    procedure SetZ(aValue: TFloat); inline;
    procedure SetW(aValue: TFloat); inline;
  public
    constructor Create(aX, aY, aZ: TFloat); overload;
    constructor Create(const aVector: TVector); overload;
    class operator Add(const aU, aV: TVector): TVector; inline;
    class operator Subtract(const aU, aV: TVector): TVector; inline;
    class operator Divide(const aVector: TVector; aFactor: TFloat): TVector; inline;
    class operator Multiply(const aVector: TVector; aFactor: TFloat): TVector; overload; inline;
    class operator Multiply(const aU, aV: TVector): TVector; overload; inline;
    class operator Negative(const aVector: TVector): TVector; inline;
    class operator Equal(const aU, aV: TVector): boolean; inline;
    class operator NotEqual(const aU, aV: TVector): boolean; inline;
    class operator GreaterThan(const aU, aV: TVector): boolean; inline;
    class operator GreaterThanOrEqual(const aU, b: TVector): boolean; inline;
    class operator LessThan(const a, b: TVector): boolean; inline;
    class operator LessThanOrEqual(const a, b: TVector): boolean; inline;

    class function Compare(const u, v: TVector): integer; static;
    class function DotProduct(const u, v: TVector): TFloat; static;
    class function Distance(const u, v: TVector): TFloat; static;
    class function AngleBetween(const u, v: TVector): TFloat; static;
    class function Angle(const u, v: TVector): TFloat; static;
    class function DotUnit(const u, v: TVector): TFloat; static;

    procedure Rotate(const aVector: TVector; const aMatrix: TMatrix);
    procedure CrossProduct(const u, v: TVector);
    procedure Normal(const v1, v2, v3: TVector);
    function IsEmpty: boolean; inline;
    procedure Clear; inline;
    function Length: TFloat;
    procedure Normalize; inline;
    function Max: TFloat; inline;
    function Min: TFloat; inline;
    procedure Abs;
    procedure Random(aMin, aMax: TFloat);
    class function Null: TVector; static;

    property Points[index: integer]: TFloat read GetIndex write SetIndex; default;
    property Pitch: TFloat read FX write SetX;
    property Yaw: TFloat read FY write SetY;
    property Roll: TFloat read FZ write SetZ;
    property X: TFloat read FX write SetX;
    property Y: TFloat read FY write SetY;
    property Z: TFloat read FZ write SetZ;
    property W: TFloat read FW write SetW;
  end;

implementation

uses
  Math,
  BB.Math;

{ TVector }

procedure TVector.Abs;
begin
  FX := System.Abs(FX);
  FY := System.Abs(FY);
  FZ := System.Abs(FZ);
end;

class operator TVector.Add(const aU, aV: TVector): TVector;
begin
  result.FX := aU.FX + aV.FX;
  result.FY := aU.FY + aV.FY;
  result.FZ := aU.FZ + aV.FZ;
end;

class function TVector.Angle(const u, v: TVector): TFloat;
var
  length: TFloat;

begin
  length := u.Length * v.Length;
  Result := DotProduct(u, v) / length;
end;

class function TVector.AngleBetween(const u, v: TVector): TFloat;
var
  m, dot: TFloat;

begin
  dot := DotProduct(u, v);
  m := u.Length * v.Length;
  Result := ArcCos(dot / m);
  if IsNan(Result) then
    Result := 0;
end;

constructor TVector.Create(aX, aY, aZ: TFloat);
begin
  FX := aX;
  FY := aY;
  FZ := aZ;
  FW := 1;
end;

class function TVector.Compare(const u, v: TVector): integer;
var
  d1, d2: TFloat;

begin
  d1 := u.Length;
  d2 := v.Length;

  if d2 > d1 then
    result := -1
  else
    if d1 > d2 then
      result := 1
    else
      result := 0;
end;

constructor TVector.Create(const aVector: TVector);
begin
  FX := aVector.FX;
  FY := aVector.FY;
  FZ := aVector.FZ;
  FW := aVector.FW;
end;

procedure TVector.CrossProduct(const u, v: TVector);
begin
  FX := (u.FY * v.FZ) - (u.FZ * v.FY);
  FY := (u.FZ * v.FX) - (u.FX * v.FZ);
  FZ := (u.FX * v.FY) - (u.FY * v.FX);
end;

class function TVector.Distance(const u, v: TVector): TFloat;
var
  c: TVector;

begin
  c := u - v;
  result := c.Length;
end;

class operator TVector.Divide(const aVector: TVector; aFactor: TFloat): TVector;
var
  inv: TFloat;
  
begin
  inv := 1 / aFactor;
  Result.FX := aVector.FX * inv;
  Result.FY := aVector.FY * inv;
  Result.FZ := aVector.FZ * inv;
end;

class function TVector.DotProduct(const u, v: TVector): TFloat;
begin
  result := (u.FX * v.FX) + (u.FY * v.FY) + (u.FZ * v.FZ);
end;

class function TVector.DotUnit(const u, v: TVector): TFloat;
var
  length: TFloat;

begin
  length := u.Length * v.Length;
  result := TVector.DotProduct(u, v) * (1 / length);
end;

function TVector.IsEmpty: boolean;
begin
  result := (FX = 0) and (FY = 0) and (FZ = 0);
end;

class operator TVector.Equal(const aU, aV: TVector): boolean;
begin
  result := (aU.FX = aV.FX) and (aU.FY = aV.FY) and (aU.FZ = aV.FZ);
end;

function TVector.GetIndex(aIndex: integer): TFloat;
begin
  case aIndex of
    0: result := FX;
    1: result := FY;
    2: result := FZ;
    3: result := FW;
  else
    result := 0;
  end;
end;

class operator TVector.GreaterThan(const aU, aV: TVector): boolean;
begin
  result := aU.Length > aV.Length;
end;

class operator TVector.GreaterThanOrEqual(const aU, b: TVector): boolean;
begin
  result := aU.Length >= b.Length;
end;

function TVector.Length: TFloat;
begin
  Result := Sqrt(Sqr(FX) + Sqr(FY) + Sqr(FZ));
  if Result = 0 then
    Result := 0.00001;
end;

class operator TVector.LessThan(const a, b: TVector): boolean;
begin
  result := a.Length < b.Length;
end;

class operator TVector.LessThanOrEqual(const a, b: TVector): boolean;
begin
  result := a.Length <= b.Length;
end;

function TVector.Max: TFloat;
begin
  Result := FX;
  if FY > Result then
    Result := FY;
  if FZ > Result then
    Result := FZ;
end;

function TVector.Min: TFloat;
begin
  Result := FX;
  if FY < Result then
    Result := FY;
  if FZ < Result then
    Result := FZ;
end;

class operator TVector.Multiply(const aU, aV: TVector): TVector;
begin
  result.FX := aU.FX * aV.FX;
  result.FY := aU.FY * aV.FY;
  result.FZ := aU.FZ * aV.FZ;
end;

class operator TVector.Multiply(const aVector: TVector; aFactor: TFloat): TVector;
begin
  result.FX := aVector.FX * aFactor;
  result.FY := aVector.FY * aFactor;
  result.FZ := aVector.FZ * aFactor;
end;

class operator TVector.Negative(const aVector: TVector): TVector;
begin
  result.FX := -result.FX;
  result.FY := -result.FY;
  result.FZ := -result.FZ;
end;

procedure TVector.Normal(const v1, v2, v3: TVector);
begin
  CrossProduct(v2 - v1, v3 - v1);
end;

procedure TVector.Normalize;
var
  l: TFloat;

begin
  l := 1 / Length;
  FX := FX * l;
  FY := FY * l;
  FZ := FZ * l;
//  W := W * l;
end;

class operator TVector.NotEqual(const aU, aV: TVector): boolean;
begin
  result := (aU.FX <> aV.FX) or (aU.FY <> aV.FY) or (aU.FZ <> aV.FZ);
end;

class function TVector.Null: TVector;
begin
  result.Clear;
end;

procedure TVector.Random(aMin, aMax: TFloat);
begin
  FX := Rnd(aMin, aMax);
  FY := Rnd(aMin, aMax);
  FZ := Rnd(aMin, aMax);
end;

procedure TVector.Rotate(const aVector: TVector; const aMatrix: TMatrix);
begin
  FX := DotProduct(aVector, TVector(aMatrix._4x4[0]));
  FY := DotProduct(aVector, TVector(aMatrix._4x4[1]));
  FZ := DotProduct(aVector, TVector(aMatrix._4x4[2]));
end;

procedure TVector.SetIndex(aIndex: integer; const aValue: TFloat);
begin
  case aIndex of
    0: FX := aValue;
    1: FY := aValue;
    2: FZ := aValue;
    3: FW := aValue;
  end;
end;

procedure TVector.SetW(aValue: TFloat);
begin
  FW := aValue;
end;

procedure TVector.SetX(aValue: TFloat);
begin
  FX := aValue;
end;

procedure TVector.SetY(aValue: TFloat);
begin
  FY := aValue;
end;

procedure TVector.SetZ(aValue: TFloat);
begin
  FZ := aValue;
end;

class operator TVector.Subtract(const aU, aV: TVector): TVector;
begin
  result.FX := aU.FX - aV.FX;
  result.FY := aU.FY - aV.FY;
  result.FZ := aU.FZ - aV.FZ;
end;

procedure TVector.Clear;
begin
  FX := 0;
  FY := 0;
  FZ := 0;
  FW := 1;
end;

end.
All cameras, rotations, lights, etc uses this unit

A helpful exception class

Many times you find out yourself writing exceptions with many conversions, I do it this way:


unit BB.Exceptions;

interface

uses
  SysUtils;

type
  EExceptionEx = class(Exception)
  public
    constructor Create(const aMessage: string; const aParam: array of variant); virtual;
  end;

  ETooManyObjects = class(EExceptionEx);
  ETimeOut = class(EExceptionEx);
  EDatabaseException = class(EExceptionEx);
  EUserException = class(EDatabaseException);
  ECompanyException = class(EDatabaseException);
  EServerException = class(EDatabaseException);
  EDataSetException = class(EDatabaseException);
  EConexionNotFound = class(EDatabaseException);

implementation

{ EExceptionEx }

constructor EExceptionEx.Create(const aMessage: string; const aParam: array of variant);
var
  n: integer;
  Text: string;

begin
  Text := aMessage;
  for n := Low(aParam) to High(aParam) do
    Text := StringReplace(Text, '%' + IntToStr(n + 1), string(aParam[n]), []);

  inherited Create(Text);
end;

end.
So is easy to write something like:
raise EExceptionEx.Create('Invalid server %1, date %2, dummy %3', ['SRV123', Now, 1]);
And you can forget about conversions...

Class operators

I use this feature in a few types (TRGB, TMatrix and TVector), it really helps cleaning the code as you can use operators on records (note that this feature can only be used in records, not classes, what a pity...)

The available operators are:

  • Add
  • Subtract
  • Negative
  • Equal
  • NotEqual
  • IntDivide
  • Multiply
  • Implicit
  • Explicit
  • LessThan
  • GreaterThan
  • Less than or equal to
  • Greater than or equal to

They can also be overloaded, some implementations:

TRGB = record

   class operator Add(const aColorA, aColorB: TRGB): TRGB;
   class operator Subtract(const aColorA, aColorB: TRGB): TRGB;
   class operator Negative(const aColor: TRGB): TRGB; inline;
   class operator Equal(const aColorA, aColorB: TRGB): boolean; inline;
   class operator NotEqual(const aColorA, aColorB: TRGB): boolean; inline;
   class operator IntDivide(const aColor: TRGB; aFactor: cardinal): TRGB; inline;
   class operator Multiply(const aColorA, aColorB: TRGB): TRGB;
   class operator Multiply(const aColor: TRGB; aValue: cardinal): TRGB;
   //Among other methods and fields
end;

class operator TRGB.Add(const aColorA, aColorB: TRGB): TRGB;
begin
  result.FR := Clamp(aColorA.FR + aColorB.FR);
  result.FG := Clamp(aColorA.FG + aColorB.FG);
  result.FB := Clamp(aColorA.FB + aColorB.FB);
end;

class operator TRGB.IntDivide(const aColor: TRGB; aFactor: cardinal): TRGB;
begin
  result.FR := aColor.FR div aFactor;
  result.FG := aColor.FG div aFactor;
  result.FB := aColor.FB div aFactor;
  result.FA := aColor.FA div aFactor;
end;

class operator TRGB.Multiply(const aColorA, aColorB: TRGB): TRGB;
begin
  result.FR := Trunc((aColorA.FR / Solid) * (aColorB.FR / Solid) * Solid);
  result.FG := Trunc((aColorA.FG / Solid) * (aColorB.FG / Solid) * Solid);
  result.FB := Trunc((aColorA.FB / Solid) * (aColorB.FB / Solid) * Solid);
  result.FA := Trunc((aColorA.FA / Solid) * (aColorB.FA / Solid) * Solid);
end;

class operator TRGB.Equal(const aColorA, aColorB: TRGB): boolean;
begin
  result := (aColorA.FR = aColorB.FR) and (aColorA.FG = aColorB.FG) and (aColorA.FB = aColorB.FB) and (aColorA.FA = aColorB.FA);
end;

class operator TRGB.Multiply(const aColor: TRGB; aValue: cardinal): TRGB;
begin
  result.FR := Clamp(aColor.FR * aValue);
  result.FG := Clamp(aColor.FG * aValue);
  result.FB := Clamp(aColor.FB * aValue);
  result.FA := Clamp(aColor.FA * aValue);
end;

class operator TRGB.Negative(const aColor: TRGB): TRGB;
begin
  result.FR := byte(-aColor.FR);
  result.FG := byte(-aColor.FG);
  result.FB := byte(-aColor.FB);
  result.FA := byte(-aColor.FA);
end;

class operator TRGB.Subtract(const aColorA, aColorB: TRGB): TRGB;
begin
  result.FR := Clamp(aColorA.FR - aColorB.FR);
  result.FG := Clamp(aColorA.FG - aColorB.FG);
  result.FB := Clamp(aColorA.FR - aColorB.FB);
  result.FA := Clamp(aColorA.FA - aColorB.FA);
end;

class operator TRGB.NotEqual(const aColorA, aColorB: TRGB): boolean;
begin
  result := (aColorA.FR <> aColorB.FR) or (aColorA.FG <> aColorB.FG) or 
(aColorA.FB <> aColorB.FB) or (aColorA.FA <> aColorB.FA);
end;
An example:
var
  a, b, c: TRGB;

begin
  a := TRGB.Red;
  b := TRGB.Yellow;
  c := (a + b) div 2; //Add and divide operators

Class operators

I use this feature in a few types (TRGB, TMatrix and TVector), it really helps cleaning the code as you can use operators on records (note that this feature can only be used in records, not classes, what a pity...)

The available operators are:

  • Add
  • Subtract
  • Negative
  • Equal
  • NotEqual
  • IntDivide
  • Multiply
  • Implicit
  • Explicit
  • LessThan
  • GreaterThan
  • Less than or equal to
  • Greater than or equal to

They can also be overloaded, some implementations:

TRGB = record
   class operator Add(const aColorA, aColorB: TRGB): TRGB;
   class operator Subtract(const aColorA, aColorB: TRGB): TRGB;
   class operator Negative(const aColor: TRGB): TRGB; inline;
   class operator Equal(const aColorA, aColorB: TRGB): boolean; inline;
   class operator NotEqual(const aColorA, aColorB: TRGB): boolean; inline;
   class operator IntDivide(const aColor: TRGB; aFactor: cardinal): TRGB; inline;
   class operator Multiply(const aColorA, aColorB: TRGB): TRGB;
   class operator Multiply(const aColor: TRGB; aValue: cardinal): TRGB;
   //Among other methods and fields
end;

class operator TRGB.Add(const aColorA, aColorB: TRGB): TRGB;
begin
  result.FR := Clamp(aColorA.FR + aColorB.FR);
  result.FG := Clamp(aColorA.FG + aColorB.FG);
  result.FB := Clamp(aColorA.FB + aColorB.FB);
end;

class operator TRGB.IntDivide(const aColor: TRGB; aFactor: cardinal): TRGB;
begin
  result.FR := aColor.FR div aFactor;
  result.FG := aColor.FG div aFactor;
  result.FB := aColor.FB div aFactor;
  result.FA := aColor.FA div aFactor;
end;

class operator TRGB.Multiply(const aColorA, aColorB: TRGB): TRGB;
begin
  result.FR := Trunc((aColorA.FR / Solid) * (aColorB.FR / Solid) * Solid);
  result.FG := Trunc((aColorA.FG / Solid) * (aColorB.FG / Solid) * Solid);
  result.FB := Trunc((aColorA.FB / Solid) * (aColorB.FB / Solid) * Solid);
  result.FA := Trunc((aColorA.FA / Solid) * (aColorB.FA / Solid) * Solid);
end;

class operator TRGB.Equal(const aColorA, aColorB: TRGB): boolean;
begin
  result := (aColorA.FR = aColorB.FR) and (aColorA.FG = aColorB.FG) and (aColorA.FB = aColorB.FB) and (aColorA.FA = aColorB.FA);
end;

class operator TRGB.Multiply(const aColor: TRGB; aValue: cardinal): TRGB;
begin
  result.FR := Clamp(aColor.FR * aValue);
  result.FG := Clamp(aColor.FG * aValue);
  result.FB := Clamp(aColor.FB * aValue);
  result.FA := Clamp(aColor.FA * aValue);
end;

class operator TRGB.Negative(const aColor: TRGB): TRGB;
begin
  result.FR := byte(-aColor.FR);
  result.FG := byte(-aColor.FG);
  result.FB := byte(-aColor.FB);
  result.FA := byte(-aColor.FA);
end;

class operator TRGB.Subtract(const aColorA, aColorB: TRGB): TRGB;
begin
  result.FR := Clamp(aColorA.FR - aColorB.FR);
  result.FG := Clamp(aColorA.FG - aColorB.FG);
  result.FB := Clamp(aColorA.FR - aColorB.FB);
  result.FA := Clamp(aColorA.FA - aColorB.FA);
end;

class operator TRGB.NotEqual(const aColorA, aColorB: TRGB): boolean;
begin
  result := (aColorA.FR <> aColorB.FR) or (aColorA.FG <> aColorB.FG) or 
(aColorA.FB <> aColorB.FB) or (aColorA.FA <> aColorB.FA);
end;
An example:
var
  a, b, c: TRGB;

begin
  a := TRGB.Red;
  b := TRGB.Yellow;
  c := (a + b) div 2; //Add and divide operators

Tuesday, October 19, 2010

Tinychess changes its name, now it will be called ChessKISS

I've found out that there is another project called TinyChess, so well, expect a new version very soon

Another game example

This is another example creating a three layer space game with a few lines of code:


unit Game;

interface

uses
  BB.Screen, BB.Screen.Sprites, BB.Input, BB.Screen.Engines, BB.Game, 
BB.Screen.Interfaces, BB.Screen.Types, BB.Colors, BB.Sound;

type
  TWave = class(T2DGame)
  private
    FBack1,
    FBack2,
    FBack3: TBackground;
    FShip: TAnimationSprite;
    FExplosion: ISound;

    procedure HandleShip;
    procedure HandleEnemies;
    procedure Fire;
  protected
    procedure Update(aEllapsedTime: Int64); override;
    procedure Draw(aEllapsedTime: Int64); override;
    procedure Initialize; override;
    procedure Uninitialize; override;
  end;

  TGameSprite = class(TAnimationSprite)
  protected
    function Game: IGame;
  end;

  TEnemy1 = class(TGameSprite)
  end;

  TShoot = class(TGameSprite)
  public
    function Render: integer; override;
  end;

  THero = class(TGameSprite)
  public
    function Render: integer; override;
  end;

implementation

{ TShoot }

function TShoot.Render: integer;
var
  i, x, y: integer;
  explosion: TGameSprite;
  Enemy1: ISprite;

begin
  result := inherited Render;

  for i := 0 to Engine.Count - 1 do
  begin
    enemy1 := Engine[i];
    if (enemy1 is TEnemy1) and (Collision(enemy1, x, y)) then
    begin
      Game.CreateThreadedSound('explosion').Run;

      explosion := TGameSprite.Create;
      explosion.Engine := Engine;
      explosion.AnimationName := 'explosion';
      explosion.CenterFrom(TSimpleSprite(Enemy1));

      Die;
      Enemy1.Die;
    end;
  end;
end;

{ THero }

function THero.Render: integer;
var
  enemy1: ISprite;
  x, y, i: integer;
  explosion: TGameSprite;

begin
  result := inherited;

  for i := 0 to Engine.Count - 1 do
  begin
    enemy1 := Engine[i];
    if (enemy1 is TEnemy1) and (Collision(enemy1, x, y)) then
    begin
      Game.CreateThreadedSound('explosion').Run;

      explosion := TGameSprite.Create;
      explosion.Engine := Engine;
      explosion.AnimationName := 'explosion';
      explosion.CenterFrom(TSimpleSprite(Enemy1));

      Enemy1.Die;
    end;
  end;
end;

{ TWave }

procedure TWave.Draw(aEllapsedTime: Int64);
begin
  FBack3.Render;
  FBack2.Render;
  FBack1.Render;

  inherited;
end;

procedure TWave.HandleEnemies;
const
  ENEMY1_SPEED = 3;
  ENEMY1_ALIVE = 2;

var
  enemy1: TAnimationSprite;
  i: integer;

begin
  if Random(100) = 73 then
  begin
    for i := 1 to 10 do
    begin
      enemy1 := TEnemy1.Create;
      enemy1.Engine := Engines[0];
      enemy1.AnimationName := 'enemy1';
      enemy1.VerticalSpeed := ENEMY1_SPEED;
      enemy1.X := GraphicsInfo.GetWidth div 10 * i;
      enemy1.Y := -enemy1.Height;
      enemy1.TimeOut := GraphicsInfo.GetMaxFPS * ENEMY1_ALIVE;
      enemy1.CollisionShape := csAuto;

      if Random(2) = 1 then
        enemy1.AngleSpeed := 0.5
      else
        enemy1.AngleSpeed := -0.5;
    end;
  end;
end;

procedure TWave.HandleShip;
const
  SHIP_SPEED = 2; //Seconds

begin
  FShip.AnimationName := 'main';

  if Input.Keyboard[DIK_LEFT] then
  begin
    FShip.AnimationName := 'main_left';
    FShip.MoveLeft(SHIP_SPEED, True);
  end;

  if Input.Keyboard[DIK_RIGHT] then
  begin
    FShip.AnimationName := 'main_right';
    FShip.MoveRight(SHIP_SPEED, True);
  end;

  if Input.Keyboard[DIK_UP] then
    FShip.MoveUp(SHIP_SPEED, True);

  if Input.Keyboard[DIK_DOWN] then
    FShip.MoveDown(SHIP_SPEED, True);

  if (Input.Keyboard[DIK_Z]) and (GraphicsInfo.GetTotalFrames mod 7 = 0) then
    Fire;

  if Input.Keyboard[DIK_ESCAPE] then
    Stop;
end;

procedure TWave.Initialize;
begin
  inherited;

  InputDevices := [idKeyboard];

  GraphicsInfo.SetMaxFPS(100);
  GraphicsInfo.SetScreenFlags([]);

  Engines.Add;
  Engines[0].FileNames.Add('ship.bmp');
  Engines[0].FileNames.Add('shoot.bmp');
  Engines[0].FileNames.Add('enemy1.bmp');
  Engines[0].FileNames.Add('explosion.bmp');
  Engines[0].LoadFile;

  FBack1 := TBackground.Create;
  FBack1.FileName := 'layer1.bmp';
  FBack1.LoadFile(TRGB.Create(255, 0, 255).ToInt(32));
  FBack1.Zoom := Round(GraphicsInfo.GetWidth / FBack1.Width * 100);
  FBack1.Seamless := True;
  FBack1.Filter := sfPoint;

  FBack2 := TBackground.Create;
  FBack2.FileName := 'layer2.bmp';
  FBack2.LoadFile(TRGB.Create(255, 0, 255).ToInt(32));
  FBack2.Zoom := Round(GraphicsInfo.GetWidth / FBack2.Width * 100);
  FBack2.Seamless := True;
  FBack2.Filter := sfPoint;

  FBack3 := TBackground.Create;
  FBack3.FileName := 'layer3.bmp';
  FBack3.LoadFile(TRGB.Create(255, 0, 255).ToInt(32));
  FBack3.Zoom := Round(GraphicsInfo.GetWidth / FBack3.Width * 100);
  FBack3.Seamless := True;
  FBack3.Filter := sfPoint;

  FShip := THero.Create;
  FShip.Engine := Engines[0];
  FShip.AnimationName := 'main';
  FShip.GoCenterX;
  FShip.Y := GraphicsInfo.GetHeight - FShip.Height;
  FShip.CollisionShape := csAuto;
  FShip.Registrator := self;

  FExplosion := LoadSound('explosion');
end;

procedure TWave.Uninitialize;
begin
  FBack1.Free;
  FBack2.Free;
  FBack3.Free;

  inherited;
end;

procedure TWave.Fire;
const
  SHOOT_SPEED = 4;
  SHOOT_ALIVE = 2;

var
  shoot: TAnimationSprite;

begin
  shoot := TShoot.Create;
  shoot.Engine := Engines[0];
  shoot.AnimationName := 'shoot';
  shoot.VerticalSpeed := -SHOOT_SPEED;
  shoot.X := FShip.X;
  shoot.Y := FShip.Y;
  shoot.TimeOut := GraphicsInfo.GetMaxFPS * SHOOT_ALIVE;
  shoot.Registrator := self;

  shoot := TShoot.Create;
  shoot.Engine := Engines[0];
  shoot.AnimationName := 'shoot';
  shoot.VerticalSpeed := -SHOOT_SPEED;
  shoot.X := FShip.Right - shoot.Width;
  shoot.Y := FShip.Y;
  shoot.TimeOut := GraphicsInfo.GetMaxFPS * SHOOT_ALIVE;
  shoot.Registrator := self;
end;

procedure TWave.Update(aEllapsedTime: Int64);
begin
  inherited;

  HandleShip;
  HandleEnemies;

  FBack3.MoveDown(2);
  FBack2.MoveDown(1);
  FBack1.MoveDown(0.5);
end;

{ TGameSprite }

function TGameSprite.Game: IGame;
begin
  result := TWave(Registrator.AsObject);
end;

end.
The overview is:
  • Loading the necessary stuff in Initialize()
  • Draw the backgrounds in Draw(), the bitmaps are automatically draw via Engine class
  • Handling the logic in Update()
    • Move ship (using the keyboard)
    • Move enemies
    • Move background
  • Unloading all suff in Uninitialize()
    
    

    Wednesday, October 13, 2010

    Tiny chess VS Crafty

    What happened had to happen:

    -----------------Crafty_1815_P4_SMP-----------------
    Crafty_1815_P4_SMP - TinyChess : 3,0/3 3-0-0 (111) 100% +1200
    -----------------TinyChess-----------------
    TinyChess - Crafty_1815_P4_SMP : 0,0/3 0-3-0 (000)   0% -1200


    No chance to win...

    Finally, Gerbil beaten

    Well, don't even consider TinyChess as a good engine, I don't post the losses, I'm just happy that I win sometimes some engines better than mine...

    Look at some search numbers, TinyChess went to deep 14 with searching 303.303 nodes while Gerbil searched 8.068.442 nodes at deep 11, strange, right?

    Another weird thing is the nodes per second that Arena counts for TinyChess:


    21:33:34, Send: 1 1455 0 43 g2xh3 Kg6f7 
    21:33:34, Send: 2 1455 0 47 g2xh3 Kg6f7 a7 
    21:33:34, Send: 3 1455 0 129 g2xh3 Kg6f7 a7 Kf7e6 
    21:33:34, Send: 4 1455 0 211 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 
    21:33:34, Send: 5 1455 0 331 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 
    21:33:34, Send: 6 1455 16 483 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q 
    21:33:34, Send: 7 1455 0 2257 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q Kf5e6 
    21:33:34, Send: 8 1455 16 4252 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q Kf5e6 Qa8g8 
    21:33:34, Send: 9 1520 15 7528 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q Kf5e6 Qa8g8 Ke6d7 
    21:33:34, Send: 10 1520 32 11876 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q Kf5e6 Qa8g8 Ke6d7 Qg8f7 
    21:33:34, Send: 11 1520 62 23351 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q Kf5e6 Qa8g8 Ke6d7 Qg8f7 Kd7c6 
    21:33:34, Send: 12 1520 94 42029 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6f5 a8=Q Kf5e6 Qa8g8 Ke6d7 Qg8f7 Kd7c6 Qf7f6 
    21:33:35, Send: 13 1540 469 150661 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6d7 a8=Q Kd7e7 Qa8e4 Ke7d7 Qe4xh4 Kd7e6 Qh4f6 Ke6d7 
    21:33:35, Send: 14 1560 640 303303 g2xh3 Kg6f7 a7 Kf7e6 Bd6e5 Ke6d7 a8=Q Kd7e7 Qa8a3 Ke7d7 Qa3d6 Kd7e8 Be5f6 Ke8f7 Qd6e7 


    The sum of movements is 546.501 in one second, so nodes search per second equal 546.501, not 47.391 as Arena says..., strange


    I'm going to prove a loss of TinyChess, the opponent will be Crafty, so for sure I know the result,  I will post it in a few minutes.

    A new victim, BigLion

    After all, the engine is not that bad...

    I created last night a tournament among all available engines, I will show the results in another post.

    Scanning a surface to get a shape

    In BB.Screen.Extras there is a very useful class called TSurface2Poly which is used in the collision class, its only purpose is to scan a surface and create a set of vertices (aka polygon), the skeleton looks like this:


    TSurface2Poly = class
    public
      constructor Create(aSurface: ISurface);
      destructor Destroy; override;
      function Execute: IShape;
      function ToString: string; override;
    
      property Tolerancy: integer read FTolerancy write FTolerancy;
      property Z: TFloat read FZ write FZ;
    end;
    You pass an ISurface as parameter and you get a IShape in response, quite easy. 
    The Z means that the vertices will set the Z value according to this property, then the other property is Tolerancy, this is a funny one, let me explain it.
    The main process is something like silhouette detection (not sophisticated in any aspect!), from a given surface we scan all pixels taking into account gaps and 
    direction changes.
    The secondary process takes the resultant shape and tries to optimize it (take 
    into account that a shape for an average surface might have thousands vertices, 
    for example, the collision test between two shapes with 10.000 produces a terrible
    amount of comparisons, which ends up in slowness), this is where the Tolerancy 
    values plays his role, I only add vertices that had an angle greater to the 
    property with the previous vertex, I know is a bit silly process, but for now it works and the function is adequately isolated, so rewrite it should be easy.

    The surface

    After the scan


    Depending on the tolerancy it might end up into a rhombus, that is up to you...
    The main adventage of this way of handling collisions is that you don't have to care if the sprite is rotated or zoomed, the collisions still works (in counterpart with pixel collisions)