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.