unit Frost.GameObjects;

interface

uses
  _Geometry,
  Frost.Utils;

type

TGameObject = class;
{$M+}
TComponent = class
  private
    FGameObject: TGameObject;
    procedure AfterConstruction; override;
  protected
    FEnable: Boolean;
    procedure SetGameObject(GameObject: TGameObject);
  public
    constructor Create; overload;
    constructor Create(GameObject: TGameObject); overload;
    procedure OnStart; virtual;
    //TODO - set on default TComponent.OnUpdate action of deleting from update list
    procedure OnUpdate(const delta: Double); virtual;
    procedure OnDestroy; virtual;
    destructor Destroy; override;
    property GameObject: TGameObject read FGameObject;
    property Enable: Boolean read FEnable write FEnable;
  end;
{$M-}

TGameObject = class sealed (TObject)
  private
    FName: string;
    FParent: TGameObject;
    FLocalView,              // for unfreeze only - local coords
    FView: TMatrix4;
    FEnabled: Boolean;       // freeze / unfreese object process
    FNeedUpdate: Boolean;
    FLayer: Byte;            // bitMask for camera
    FDestroyMarker: Boolean;
    procedure CreateGameObject(
        Parent: TGameObject;
        Position: TVector3;
        Rotation: TMatrix3);
    function GetGlobalPosition: TVector3;
    procedure SetGlobalPosition(Position: TVector3);
    function GetLocalPosition: TVector3;
    procedure SetLocalPosition(Position: TVector3);   
    function GetLocalScale: TVector3;
    procedure SetLocalScale(Scale: TVector3);
    function GetLocalView: TMatrix4;
    procedure SetLocalView(LocalView: TMatrix4);
    function GetGlobalRotation: TMatrix3;
    procedure SetGlobalRotation(Rotation: TMatrix3);       
    function GetRotation: TMatrix3;
    procedure SetRotation(Rotation: TMatrix3);          
    procedure UpdatePosition;
  protected
    procedure AfterConstruction; override;
    procedure Freeze;
    procedure UnFreeze;
    function GetEnabled: Boolean;
    procedure SetEnabled(Enabled: Boolean);
    procedure SetParent(Parent: TGameObject);
  public
    constructor Create(
        Parent: TGameObject;
        Position: TVector3;
        EulerAngles: TVector3); overload;   
    constructor Create(
        Parent: TGameObject;
        Position: TVector3;
        Rotation: TMatrix3); overload;
    constructor Create(
        Parent: TGameObject;
        Position: TVector3); overload;
    constructor Create(
        Parent: TGameObject = nil); overload;
    procedure AddComponent(Component: TComponent);
    procedure DestroyMe;
    property Enabled: Boolean         read GetEnabled         write SetEnabled;
    property Position: TVector3       read GetGlobalPosition  write SetGlobalPosition;
    property LocalPosition: TVector3  read GetLocalPosition   write SetLocalPosition;
    property LocalScale: TVector3     read GetLocalScale      write SetLocalScale;
    property Rotation: TMatrix3       read GetGlobalRotation  write SetGlobalRotation;
    property LocalRotation: TMatrix3  read GetRotation        write SetRotation;
    property LocalView: TMatrix4      read GetLocalView       write SetLocalView;     
    property View: TMatrix4           read FView              ;//write SetLocalView; 
    property Parent: TGameObject      read FParent            write SetParent;      
    property Name: string             read FName              write FName;   
    property Layer: Byte              read FLayer             write FLayer;
  end;

TObjectTree = class sealed
  const COLLAPSE_PERIOD = 20;
  private class var FCurrentInstance: TObjectTree;
  private
    // Object list
    FObjectList: array of TGameObject;
    FObjectCount,
    FMaxObjectCount: Integer;
    // Root of tree
    FRootObject: TGameObject;
    // linkecd component list
    FComponentList: array of TComponent;
    FComponentCount,
    FMaxComponentCount: Integer;
    // Counter for long operations
    FCollapseCounter: Byte;
    function AddObject(GameObject: TGameObject): TGameObject;
    function AddComponent(Component: TComponent): TComponent;   
    procedure CheckConsistensy(GameObject: TGameObject);
    procedure DestroyLinkedComponents(GameObject: TGameObject);
    procedure CollapseNilObjects();                      
    procedure CollapseNilComponents();
  public
    constructor Create; overload;     
    destructor Destroy; override;
    procedure Update(const delta: Double);
  end;

implementation

const DEFAULT_LAYER = 1;

{$REGION 'TComponent'}
procedure TComponent.SetGameObject(GameObject: TGameObject);
begin
  if (GameObject = nil) then
    FGameObject := TObjectTree.FCurrentInstance.FRootObject
  else
    FGameObject := GameObject;
  FEnable := True;
  OnStart;
end;

constructor TComponent.Create;
begin
  SetGameObject(nil);
end;

constructor TComponent.Create(GameObject: TGameObject);
begin
  SetGameObject(GameObject);
end;

destructor TComponent.Destroy;
begin
  OnDestroy;
  inherited;
end;

procedure TComponent.AfterConstruction;
begin
  if (TObjectTree.FCurrentInstance <> nil) then
     TObjectTree.FCurrentInstance.AddComponent(Self);
end;

procedure TComponent.OnStart;
begin

end;

procedure TComponent.OnUpdate(const delta: Double);
begin

end;

procedure TComponent.OnDestroy;
begin

end;
{$ENDREGION}

{$REGION 'TGameObject'}
{$REGION 'Create'}
procedure TGameObject.CreateGameObject(
        Parent: TGameObject;
        Position: TVector3;
        Rotation: TMatrix3);
begin
  FDestroyMarker := false;
  FLayer := DEFAULT_LAYER;
  FLocalView := Matrix4(IDENTITY_MATRIX3, Position);
  FLocalView := FLocalView * Rotation;  
  FParent := Parent;
  if FParent = nil then
    FParent := TObjectTree.FCurrentInstance.FRootObject;
  if FParent = nil then           
    FView := FLocalView
  else
    FView := FParent.FView * FLocalView;
end;

constructor TGameObject.Create(
        Parent: TGameObject;
        Position: TVector3;
        EulerAngles: TVector3);
begin
  CreateGameObject(Parent, Position, RotateFromAngles(EulerAngles));
end;

constructor TGameObject.Create(
        Parent: TGameObject;
        Position: TVector3;
        Rotation: TMatrix3);
begin
  CreateGameObject(Parent, Position, Rotation);
end;

constructor TGameObject.Create(
        Parent: TGameObject;
        Position: TVector3);
begin
  CreateGameObject(Parent, Position, IDENTITY_MATRIX3);
end;

constructor TGameObject.Create(
        Parent: TGameObject = nil);
begin
  CreateGameObject(Parent, VECTOR3_NULL, IDENTITY_MATRIX3);
end;
{$ENDREGION}

procedure TGameObject.AddComponent(Component: TComponent);
begin
  if (Component <> nil) then
  begin
    Component.FGameObject := Self;
    Component.FEnable := true;
  end;
end;

procedure TGameObject.DestroyMe;
begin
  FDestroyMarker := True;
end;

procedure TGameObject.UpdatePosition;
begin
  if (FParent = nil) then
    exit;
  if (FParent.FNeedUpdate) or (FNeedUpdate) then
  begin
    FView := FLocalView * FParent.FView;
    FNeedUpdate := True;
  end;
end;

procedure TGameObject.AfterConstruction;
begin
  if (TObjectTree.FCurrentInstance <> nil) then
     TObjectTree.FCurrentInstance.AddObject(Self);
end;
{$REGION 'Get/Set'}
procedure  TGameObject.Freeze;
begin
  //todo
  //Save local position to FParentRelativePosition if freese
  FEnabled := false;
end;

procedure  TGameObject.UnFreeze;
begin
  //todo
  //Restore global position from local if unfreese
  FEnabled := true;
end;

function TGameObject.GetEnabled: Boolean;
begin
  Result := FEnabled;
end;

procedure TGameObject.SetEnabled(Enabled: Boolean);
begin
  if FEnabled = Enabled then
    Exit; //nothing need to do 
  if FEnabled then
    Freeze
  else
    UnFreeze;
end;

procedure TGameObject.SetParent(Parent: TGameObject);
begin
  FParent := Parent;
  if (TObjectTree.FCurrentInstance <> nil) then
    TObjectTree.FCurrentInstance.CheckConsistensy(Parent);
end;

function TGameObject.GetGlobalPosition: TVector3;
begin
  Result:= Vector3(FView._30, FView._31, FView._32);
end;

procedure TGameObject.SetGlobalPosition(Position: TVector3);
begin
  FView._30:= Position.X;
  FView._31:= Position.Y;
  FView._32:= Position.Z;
  FNeedUpdate:= true;
end;

function TGameObject.GetLocalPosition: TVector3;
begin
  Result:= Vector3(FLocalView._30, FLocalView._31, FLocalView._32);
end;

procedure TGameObject.SetLocalPosition(Position: TVector3);
begin
  //Position := FLocalView.Transpose * Position;
  FLocalView._30:= Position.X;
  FLocalView._31:= Position.Y;
  FLocalView._32:= Position.Z;
  if (FParent = nil) then
    FView:= FLocalView
  else
    FView:= FLocalView * FParent.FView;
  FNeedUpdate:= true;
end;

function TGameObject.GetLocalScale: TVector3;
begin
  Result:= Vector3(FLocalView._00, FLocalView._11, FLocalView._22);
end;

procedure TGameObject.SetLocalScale(Scale: TVector3);
begin
  FLocalView._00:= Scale.X;
  FLocalView._11:= Scale.Y;
  FLocalView._22:= Scale.Z;  
  if (FParent = nil) then
    FView:= FLocalView
  else
    FView:= FLocalView * FParent.FView;
  FNeedUpdate:= true;
end;

function TGameObject.GetGlobalRotation: TMatrix3;
begin
  Result := FView;
end;

procedure TGameObject.SetGlobalRotation(Rotation: TMatrix3);
begin
  FView:= Matrix4(Rotation, Vector3(FView._30, FView._31, FView._32));
  FNeedUpdate:= true;
end;

function TGameObject.GetRotation: TMatrix3;
begin                  
  //todo - delete FParent<>nil if and create root object
  Result := FLocalView;
end;

procedure TGameObject.SetRotation(Rotation: TMatrix3);
begin
  //todo - delete FParent<>nil if and create root object
//  if FParent <> nil then
//    FView:= FParent.FView * FLocalView
//  else
//    FView:= FLocalView;
  FLocalView := Matrix4(Rotation, Vector3(FLocalView._30, FLocalView._31, FLocalView._32));  
  FNeedUpdate := true;
end;

function TGameObject.GetLocalView: TMatrix4;
begin                  
  //todo - delete FParent<>nil if and create root object
  if FParent <> nil then
    Result:= FParent.FView.Transpose * FView
  else
    Result:= FView;
end;

procedure TGameObject.SetLocalView(LocalView: TMatrix4);
begin
  //todo - delete FParent<>nil if and create root object
  if FParent<>nil then
    begin
      FView:= FParent.FView * LocalView;
      FLocalView:= LocalView;
    end
  else
    begin
      FView:= LocalView;
      FLocalView:= LocalView;
    end;
end;
{$ENDREGION}
{$ENDREGION}

{$REGION 'TObjectTree'}
function  TObjectTree.AddObject(GameObject: TGameObject): TGameObject;
begin                             
  if (FObjectCount >= FMaxObjectCount) then
    begin
      FMaxObjectCount := FMaxObjectCount + 50;
      SetLength(FObjectList, FMaxObjectCount + 1);
    end;
  FObjectList[FObjectCount] := GameObject;
  GameObject.Name := IntToStr(FObjectCount);
  Inc(FObjectCount);
  Result := GameObject;
end;

procedure TObjectTree.CheckConsistensy(GameObject: TGameObject);
var
  i: Word;
  Position: Word;
  Temp: TGameObject;
begin
//TODO  check all
//  for i := 1 to FObjectCount do
//    if (FObjectList[i] <> nil) then
//      WriteLn(FObjectList[i].FName);
//  WriteLn('=================');
  Position := 0;
  for i := 1 to FObjectCount do
    if (FObjectList[i] <> nil) then
      if (FObjectList[i] = GameObject) then
      begin
        Position := i;
        Break;
      end;
  if position = 0 then
    Exit;
  for i := 1 to position - 1 do
    if (FObjectList[i] <> nil) then
      if (FObjectList[i].FParent <> nil) then
        if (FObjectList[i].FParent = GameObject) then
        begin
          Temp := FObjectList[i];
          FObjectList[i] := FObjectList[Position];
          FObjectList[Position] := Temp;
          CheckConsistensy(Temp);
          Break;
        end;
//  for i := 1 to FObjectCount do
//    if (FObjectList[i] <> nil) then
//      WriteLn(FObjectList[i].FName);
//  WriteLn('=================');
end;

function  TObjectTree.AddComponent(Component: TComponent): TComponent;
begin
  if (FComponentCount >= FMaxComponentCount) then
    begin
      FMaxComponentCount := FMaxComponentCount + 50;
      SetLength(FComponentList, FMaxComponentCount + 1);
    end;
  FComponentList[FComponentCount] := Component;  
  Inc(FComponentCount);
  Result := Component;
end;

constructor TObjectTree.Create();
begin
  inherited;
  FCurrentInstance := Self;
  FObjectCount := 0;
  FMaxObjectCount := 0;    
  FComponentCount := 0;
  FMaxComponentCount := 0;
  FRootObject := TGameObject.Create(nil, VECTOR3_NULL, VECTOR3_NULL);
end;

procedure FreeAndNil(var Obj);
var
  Temp: TObject;
begin
  Temp := TObject(Obj);
  Pointer(Obj) := nil;
  Temp.Free;
end;

procedure TObjectTree.DestroyLinkedComponents(GameObject: TGameObject);
var
  i: Word;
begin
  // Destroy all components on cleared GameObjects
  for i := 0 to FComponentCount - 1 do
    if (FComponentList[i] <> nil) then
      if (FComponentList[i].FGameObject = GameObject) then
      begin
        FreeAndNil(FComponentList[i]);
      end;
end;

procedure TObjectTree.CollapseNilObjects();
var
  i, shift: Word;
begin
  shift := 1;
  for i := 1 to FObjectCount - 1 do
    if (FObjectList[i] = nil) then
    begin
      while (FObjectList[i + shift] = nil) and (i + shift < FObjectCount - 1) do
        Inc(shift);
      FObjectList[i] := FObjectList[i + shift];
      FObjectList[i + shift] := nil;   
      if (i + shift = FObjectCount - 1) then
      begin
        FObjectCount := FObjectCount - shift;
        break;
      end;
    end;
end;

procedure TObjectTree.CollapseNilComponents();    
var
  i, shift: Word;
begin
  shift := 1;
  for i := 0 to FComponentCount - 1 do
    if (FComponentList[i] = nil) then
    begin
      while (FComponentList[i + shift] = nil) and (i + shift < FComponentCount - 1) do
        Inc(shift);
      FComponentList[i] := FComponentList[i + shift];
      FComponentList[i + shift] := nil;   
      if (i + shift = FComponentCount - 1) then
      begin
        FComponentCount := FComponentCount - shift;
        break;
      end;
    end;
end;

procedure TObjectTree.Update(const delta: Double);
var
  i: Word;
begin
//  for i := 1 to FObjectCount - 1 do
//    if (FObjectList[i] <> nil) then
//      WriteLn(FObjectList[i].FName)
//    else                           
//      WriteLn('nil');
//  for i := 0 to FComponentCount - 1 do
//    if (FComponentList[i] <> nil) then
//      WriteLn(FComponentList[i].GameObject.FName)
//    else
//      WriteLn('nil');
//  WriteLn('=================');
  // Destroy all marked GameObjects   
  for i := 1 to FObjectCount - 1 do
    if FObjectList[i] <> nil then
      if FObjectList[i].parent.FDestroyMarker then
        FObjectList[i].FDestroyMarker := true;
  for i := 1 to FObjectCount - 1 do
    if FObjectList[i] <> nil then
      if FObjectList[i].FDestroyMarker then
        begin
          DestroyLinkedComponents(FObjectList[i]);
          FreeAndNil(FObjectList[i]);
        end;
  // Move all Objects
  for i := 1 to FObjectCount - 1 do
    if FObjectList[i] <> nil then
      FObjectList[i].UpdatePosition;
  for i := 1 to FObjectCount - 1 do
    if FObjectList[i] <> nil then
      FObjectList[i].FNeedUpdate:= false;
  // Update all Components
  for i := 0 to FComponentCount - 1 do
    if (FComponentList[i] <> nil) and (FComponentList[i].Enable) then
      FComponentList[i].OnUpdate(delta);
  // Clear memory
  if FCollapseCounter = 0 then
  begin
    FCollapseCounter := COLLAPSE_PERIOD;
    CollapseNilObjects();          
    CollapseNilComponents();
  end;
end;

destructor TObjectTree.Destroy;   
var
  i: Word;
begin         
  for i:= 0 to FObjectCount - 1 do
    if FObjectList[i] <> nil then
      begin
        FObjectList[i].Free;
      end;
  SetLength(FObjectList, 0);
  FCurrentInstance := nil;
  inherited;
end;
{$ENDREGION}

end.
