Implementation of a circle impulse physics engine simulator

 


unit CBUnit;


interface


uses

  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,  System.Generics.Collections,

  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Objects,

  FMX.Layouts;


type

  TCirclePhysics = record

    VX: Single;

    VY: Single;

    ProtectUntil: Cardinal; // 이 시각까지 생존 보호

  end;


type

  TForm1 = class(TForm)

    Button1: TButton;

    Circle1: TCircle;

    TimerPhysics: TTimer;

    LayoutRoot: TLayout;

    Circle2: TCircle;

    Circle3: TCircle;

    Circle4: TCircle;

    Timer1: TTimer;

    Text1: TText;

    procedure Button1Click(Sender: TObject);

    procedure FormCreate(Sender: TObject);

    procedure FormDestroy(Sender: TObject);

    procedure TimerPhysicsTimer(Sender: TObject);

    procedure Timer1Timer(Sender: TObject);

  private

    FCircleMap: TDictionary<TCircle, TCirclePhysics>;

     FRemoveQueue: TList<TCircle>;


    procedure UpdateCirclePhysics(ACircle: TCircle);

    procedure AddNewCircle(ASpeed: Single);

    procedure RegisterCircle(ACircle: TCircle; ASpeed: Single;  ADirection: Integer);

    procedure CheckCircleCollision(C1, C2: TCircle);

    procedure RemoveCircle(ACircle: TCircle);

    procedure SplitCircleOnWall(ACircle: TCircle; HitVerticalWall, HitHorizontalWall: Boolean);

    procedure Create_SpawnCircle;


  public

    { Public declarations }

    C_Width, C_Height : single;

  end;


var

  Form1: TForm1;


implementation


const

  C_Spped = 20;

  MIN_CIRCLE_SIZE = 40;  // ← 여기만 바꾸면 됨


  SPLIT_SPEED_FACTOR = 0.6;

  WALL_PROTECT_MS = 800; // 0.8초 (추천: 500~1200)

  MIN_AXIS_COMPONENT = 0.35; // 0.3~0.4 권장



{$R *.fmx}


procedure TForm1.Button1Click(Sender: TObject);

begin

  Button1.Visible := FALSE;


  Circle1.Position.X := LayoutRoot.Width / 3;

  Circle2.Position.X := LayoutRoot.Width / 3;

  Circle3.Position.X := LayoutRoot.Width / 3 * 2;

  Circle4.Position.X := LayoutRoot.Width / 3 * 2;


  Circle1.Position.Y := LayoutRoot.Height / 3;

  Circle3.Position.Y := LayoutRoot.Height / 3;

  Circle2.Position.Y := LayoutRoot.Height / 3 * 2;

  Circle4.Position.y := LayoutRoot.Height / 3 * 2;


  RegisterCircle(Circle1, C_Spped, 1 );  // 미리 만들어진 써클

  RegisterCircle(Circle2, C_Spped, 3 );  // 미리 만들어진 써클

  RegisterCircle(Circle3, C_Spped, 2 );  // 미리 만들어진 써클

  RegisterCircle(Circle4, C_Spped, 4 );  // 미리 만들어진 써클



  TimerPhysics.Enabled := True;

  Timer1.Enabled := TRUE;

end;


procedure TForm1.FormCreate(Sender: TObject);

begin

  C_Width  :=  200; //  120 + Random(240 - 120 + 1); // 120 ~ 260

  C_Height :=  C_Width;



  FCircleMap := TDictionary<TCircle, TCirclePhysics>.Create;


  FRemoveQueue := TList<TCircle>.Create;

end;



procedure TForm1.FormDestroy(Sender: TObject);

begin

   FCircleMap.Free;

  FRemoveQueue.Free;

end;


procedure TForm1.RegisterCircle(ACircle: TCircle; ASpeed: Single; ADirection: Integer );

var

  Phys: TCirclePhysics;

begin

  case ADirection of

    1: begin Phys.VX :=  ASpeed; Phys.VY :=  ASpeed; end;

    2: begin Phys.VX := -ASpeed; Phys.VY :=  ASpeed; end;

    3: begin Phys.VX :=  ASpeed; Phys.VY := -ASpeed; end;

    4: begin Phys.VX := -ASpeed; Phys.VY := -ASpeed; end;

  else

    Phys.VX := ASpeed;

    Phys.VY := ASpeed;

  end;


  Phys.ProtectUntil := 0; // ← 보호 초기화


  if FCircleMap.ContainsKey(ACircle) then

    FCircleMap.Remove(ACircle);


  FCircleMap.Add(ACircle, Phys);

end;


//----------------------------------------------------------

procedure TForm1.UpdateCirclePhysics(ACircle: TCircle);

const

  WALL_PUSH = 2.0;              // 벽에서 밀어내는 거리

  WALL_ANGLE_JITTER = 0.05;     // 벽 반사 시 미세 각도 교란 (라디안)

var

  Phys: TCirclePhysics;

  NextX, NextY: Single;

  HitV, HitH: Boolean;

begin

  if not FCircleMap.TryGetValue(ACircle, Phys) then

    Exit;


  HitV := False;

  HitH := False;


  // 다음 위치 예측

  NextX := ACircle.Position.X + Phys.VX;

  NextY := ACircle.Position.Y + Phys.VY;


  // -------- 좌 / 우 벽 --------

  if NextX <= 0 then

  begin

    Phys.VX := Abs(Phys.VX);

    ACircle.Position.X := WALL_PUSH; // 벽 밖으로 밀기

    HitV := True;

  end

  else if (NextX + ACircle.Width) >= LayoutRoot.Width then

  begin

    Phys.VX := -Abs(Phys.VX);

    ACircle.Position.X := LayoutRoot.Width - ACircle.Width - WALL_PUSH;

    HitV := True;

  end

  else

    ACircle.Position.X := NextX;


  // -------- 상 / 하 벽 --------

  if NextY <= 0 then

  begin

    Phys.VY := Abs(Phys.VY);

    ACircle.Position.Y := WALL_PUSH;

    HitH := True;

  end

  else if (NextY + ACircle.Height) >= LayoutRoot.Height then

  begin

    Phys.VY := -Abs(Phys.VY);

    ACircle.Position.Y := LayoutRoot.Height - ACircle.Height - WALL_PUSH;

    HitH := True;

  end

  else

    ACircle.Position.Y := NextY;


  // -------- 벽 반사 후 미세 각도 교란 --------

  if HitV or HitH then

  begin

    Phys.VX := Phys.VX + (Random - 0.5) * WALL_ANGLE_JITTER * Abs(Phys.VY);

    Phys.VY := Phys.VY + (Random - 0.5) * WALL_ANGLE_JITTER * Abs(Phys.VX);


    // 벽 충돌 시 분리 처리

    SplitCircleOnWall(ACircle, HitV, HitH);

  end;


  // 물리 정보 저장

  FCircleMap[ACircle] := Phys;

end;



//------------------------------------------------------------------

procedure TForm1.CheckCircleCollision(C1, C2: TCircle);

var

  P1, P2: TCirclePhysics;

  Dx, Dy: Single;

  Dist, MinDist: Single;

  NX, NY: Single;

  Overlap: Single;

  TempVX, TempVY: Single;

begin

  if not FCircleMap.TryGetValue(C1, P1) then Exit;

  if not FCircleMap.TryGetValue(C2, P2) then Exit;


  // 중심 거리 계산

  Dx := (C2.Position.X + C2.Width * 0.5) -

        (C1.Position.X + C1.Width * 0.5);

  Dy := (C2.Position.Y + C2.Height * 0.5) -

        (C1.Position.Y + C1.Height * 0.5);


  Dist := Sqrt(Dx*Dx + Dy*Dy);

  MinDist := (C1.Width + C2.Width) * 0.5;


  // 충돌 아님

  if (Dist <= 0) or (Dist >= MinDist) then

    Exit;


  // ---------- 1️⃣ 위치 분리 (가장 중요) ----------

  NX := Dx / Dist;

  NY := Dy / Dist;


  Overlap := MinDist - Dist;


  // 두 공을 반씩 밀어냄

  C1.Position.X := C1.Position.X - NX * (Overlap * 0.5);

  C1.Position.Y := C1.Position.Y - NY * (Overlap * 0.5);


  C2.Position.X := C2.Position.X + NX * (Overlap * 0.5);

  C2.Position.Y := C2.Position.Y + NY * (Overlap * 0.5);


  // ---------- 2️⃣ 속도 반사 (단순 교환) ----------

  TempVX := P1.VX;

  TempVY := P1.VY;


  P1.VX := P2.VX;

  P1.VY := P2.VY;


  P2.VX := TempVX;

  P2.VY := TempVY;


  FCircleMap[C1] := P1;

  FCircleMap[C2] := P2;

end;



//*********************************************************************

procedure TForm1.TimerPhysicsTimer(Sender: TObject);

var

  List: TArray<TCircle>;

  I, J: Integer;

  C: TCircle;

begin

  // 1) 스냅샷

  List := FCircleMap.Keys.ToArray;


  // 2) 이동 + 벽 처리

  for I := 0 to High(List) do

    if FCircleMap.ContainsKey(List[I]) then

      UpdateCirclePhysics(List[I]);


  // 3) 공–공 충돌

  for I := 0 to High(List) do

    for J := I + 1 to High(List) do

      if FCircleMap.ContainsKey(List[I]) and

         FCircleMap.ContainsKey(List[J]) then

        CheckCircleCollision(List[I], List[J]);


  //  4) 여기서만 실제 제거

  for C in FRemoveQueue do

  begin

    FCircleMap.Remove(C);

    C.Parent := nil;

    C.Free;

  end;


  FRemoveQueue.Clear;

end;


//-----------------------------------------------------------

// 미사용

procedure TForm1.AddNewCircle(ASpeed: Single);

var

  C: TCircle;

  OuterColor: TAlphaColor;

begin

  C := TCircle.Create(LayoutRoot);

  C.Parent := LayoutRoot;


  // ---- 아래 값들은 질문에 주신 Circle1 값을 그대로 적용 ----

  C.Fill.Kind := TBrushKind.Gradient;

  C.Fill.Gradient.Style := TGradientStyle.Radial;


  C.Width := C_Width;

  C.Height :=C_Height;


  C.Stroke.Kind := TBrushKind.None; // 원본에 Stroke 언급 없으니 일반적으로 없음 처리


  // 위치는 원본값 고정 대신 "생성용"이므로 랜덤

  C.Position.X := Random(Round(LayoutRoot.Width - C.Width));

  C.Position.Y := Random(Round(LayoutRoot.Height - C.Height));


  // ---- Gradient Points 구성 (포인트 '생성'은 필요함) ----

  C.Fill.Gradient.Points.Clear;

  C.Fill.Gradient.Points.Add; // item 0 생성

  C.Fill.Gradient.Points.Add; // item 1 생성


  // 원본 구조 그대로:

  // item0: 바깥색, Offset 0

  // item1: 흰색, Offset 1

  OuterColor :=

    $FF000000 or

    (Random(256) shl 16) or

    (Random(256) shl 8)  or

    Random(256);


  C.Fill.Gradient.Points[0].Offset := 0.0;

  C.Fill.Gradient.Points[0].Color  := OuterColor; //  바깥쪽 랜덤


  C.Fill.Gradient.Points[1].Offset := 1.0;

  C.Fill.Gradient.Points[1].Color  := $FFFFFFFF;


  // 물리 등록

  RegisterCircle(C, ASpeed, 1 ); // 일단 1

end;


//-----------------------------------------------------------------

procedure TForm1.RemoveCircle(ACircle: TCircle);

begin

  if not FRemoveQueue.Contains(ACircle) then

    FRemoveQueue.Add(ACircle);

end;


//--------------------------------------------------------------------------------------------------------------------

procedure TForm1.SplitCircleOnWall( ACircle: TCircle;  HitVerticalWall: Boolean;  HitHorizontalWall: Boolean );

var

  Phys: TCirclePhysics;

  NewCircle: TCircle;

  NewSize: Single;

  Speed: Single;

  Angle: Single;

  DX, DY: Single;

  NowTick: Cardinal;

begin

  // 물리 정보 확인

  if not FCircleMap.TryGetValue(ACircle, Phys) then

    Exit;


  NowTick := TThread.GetTickCount;


  //  보호 중이면 분리 금지

  if NowTick < Phys.ProtectUntil then

    Exit;


  // 최소 크기 도달 → 제거

  if ACircle.Width <= MIN_CIRCLE_SIZE then

  begin

    RemoveCircle(ACircle);

    Exit;

  end;


  // 현재 속도 크기 계산

  Speed := Sqrt(Phys.VX * Phys.VX + Phys.VY * Phys.VY);

  if Speed < 0.01 then

    Speed := 16;


  //  분리 시 감속 (전체 분위기 제어 포인트)

  Speed := Speed * 0.8;  // ← 원하면 0.5 ~ 0.8 조절


  // 크기 절반

  NewSize := ACircle.Width * 0.6;

  ACircle.Width  := NewSize;

  ACircle.Height := NewSize;


   // -------- 랜덤 분리 방향 --------

  repeat

    Angle := Random * 2 * Pi;

    DX := Cos(Angle);

    DY := Sin(Angle);

  until (Abs(DX) > MIN_AXIS_COMPONENT) and

        (Abs(DY) > MIN_AXIS_COMPONENT);


  // -------- 기존 공 --------

  // 위치 분리 (겹침 방지)

  ACircle.Position.X := ACircle.Position.X + DX * NewSize;

  ACircle.Position.Y := ACircle.Position.Y + DY * NewSize;


  // 속도: 방향만 변경, 크기 유지

  Phys.VX :=  Speed * DX;

  Phys.VY :=  Speed * DY;


  // 보호 시작

  Phys.ProtectUntil := NowTick + WALL_PROTECT_MS;

  FCircleMap[ACircle] := Phys;


  // -------- 새 공 --------

  NewCircle := TCircle.Create(LayoutRoot);

  NewCircle.Parent := LayoutRoot;


  NewCircle.Width  := NewSize;

  NewCircle.Height := NewSize;


  // 반대 방향으로 충분히 분리

  NewCircle.Position.X := ACircle.Position.X - DX * NewSize * 2;

  NewCircle.Position.Y := ACircle.Position.Y - DY * NewSize * 2;


  // 외형 유지 (그라디언트 그대로)

  NewCircle.Fill.Assign(ACircle.Fill);

  NewCircle.Stroke.Assign(ACircle.Stroke);


  // 새 공 물리 (정확히 반대 방향)

  Phys.VX := -Speed * DX;

  Phys.VY := -Speed * DY;

  Phys.ProtectUntil := NowTick + WALL_PROTECT_MS;


  FCircleMap.Add(NewCircle, Phys);

end;



//---------------------------------------------------------------

procedure TForm1.Create_SpawnCircle;

var

  C: TCircle;

  OuterColor: TAlphaColor;

begin

  C := TCircle.Create(LayoutRoot);

  C.Parent := LayoutRoot;


  // ---- 크기 ----

  C.Width  := 200;  // 120 + Random(240 - 120 + 1);

  C.Height := C.Width;


  // ---- 위치 (화면 안 랜덤) ----

  C.Position.X := Random(Round(LayoutRoot.Width  - C.Width));

  C.Position.Y := Random(Round(LayoutRoot.Height - C.Height));


  // ---- Gradient (Radial) ----

  C.Fill.Kind := TBrushKind.Gradient;

  C.Fill.Gradient.Style := TGradientStyle.Radial;


  // 포인트 초기화

  C.Fill.Gradient.Points.Clear;


  //  포인트 2개 "생성" (색 지정은 나중에 대입)

  C.Fill.Gradient.Points.Add;

  C.Fill.Gradient.Points.Add;


  // 바깥쪽 색상 (랜덤)

  OuterColor :=

    $FF000000 or

    (Random(256) shl 16) or

    (Random(256) shl 8)  or

    Random(256);


  // FMX 템플릿 구조 그대로:

  // item0: 바깥색, Offset 0

  // item1: 센터 흰색, Offset 1

  C.Fill.Gradient.Points[0].Offset := 0.0;

  C.Fill.Gradient.Points[0].Color  := OuterColor;


  C.Fill.Gradient.Points[1].Offset := 1.0;

  C.Fill.Gradient.Points[1].Color  := $FFFFFFFF; // white


  // ---- 테두리 제거 ----

  C.Stroke.Kind := TBrushKind.None;


  // ---- 물리 등록 ----

  RegisterCircle(C, C_Spped, 1 + Random(4));

end;


procedure TForm1.Timer1Timer(Sender: TObject);

begin

  Text1.Text := FCircleMap.Count.ToString;


  if FCircleMap.Count < 100 then // 전체 공 갯수 제한.

     Create_SpawnCircle();

end;




end.



댓글 없음:

댓글 쓰기