공부/SFML

좀비 준비, 마우스 회전 적용, 충돌 검사, sf::View, 카메라 플레이어 따라가기, 타일맵, vertex, Transform, 좀비 추가 방법

월러비 2025. 7. 7. 23:54

짧은 설명

  • 브런치 이름으로 하는 대표적인 이름
    • hitfix : 오류난 기능을 고칠 떄 사용하는 이름
    • release : 출시할 내용을 넣을 때 - 배포본이 되는 단계
    • feature기능이름 : 새로운 기능을 개발할 떄
    • 이 세 브런치의 이름은 만들고 지우고를 반복하게 된다.
  • main : 오류가 없어야한다.
  • 파일을 복사하여 가지고다니지 말고 브런치를 푸쉬해서 원격으로 올린 다음에 그것을 클론해서 개발하는것이 좋다.
  • 축 입력떄 1P 2P 축을 define에서 선언하고, InputMgr에서 새로 만든다음에 사용하는것이 조건문을 길게 쓰지 않아도 되서 좋다.
  • 2P가 있을떄는 새로운 클래스를 만들기 보다는 한 클래스에서 bool이나 int의 조건으로 2P를 분할할 수 있게 하는것이 좋다.
  • score같은것은 SceneGame에 넣는것이 좋다.
    • SceneGame은 해당 씬을 관리할 수 있는 관리자 역할이기 떄문이라고 한다.

좀비 준비

  • Util
    • RandomOnUnitCircle : 사용자가 정의한 반경 안에서 원 위의 점을 랜덤으로 반환하는 함수
    • RandomInUnitCircle : 사용자가 정의한 반경 안에서 원 안에 점을 랜덤으로 반환하는 함수
    • CheckCollision : 두 사각형 충돌 체크
      • 회전한 사각형 충돌 검사도 이 함수로 가능하다.
  • FloatRect
    • contains : 볼이랑 배트 충돌에서 사용한 함수다.
  • 회전한 사각형 충돌 검사 방법
    • 닿는곳의 벡터와 꼭짓점의 벡터를 체크한다.

플레이어

마우스 회전 적용

  • Utils
    • RadianToDegree : 라디안 각도를 디그리로 변환
      • 라디안 : 파이 : 180도다.
      • 디그리 = 라디안 * 180 / 파이
    • DegreeToRadian : 디그리 각도를 라디안으로 변환
      • 라디안 = 디그리 * 파이 / 180
    • AngleRadian : 각도가 라디안으로 받아진다.
      • 위의 함수로 각도를 변환해줘야한다.
      • 벡터가 어떤 방향(각도)를 가리키는지 ‘라디안’으로 반환
    • Angle : 각도가 디그리로 받아진다.
      • 벡터가 어떤 방향(각도)를 가리키는지 ‘디그리’로 반환
    • Dot : 내적
      • 각도 비교 등에 사용된다.
        • 기준 벡터 ~ 찾는 벡터까지의 각도를 알 수 있게 된다.
      • 앞 ~ 뒤까지의 각도를 알 수 있다.
  • look 벡터 : 각도가 구해진다.
    • SetRotaiton으로 넣으면 마우스를 바라보게 된다.
    • 플레이어Position - 마우스Postion = 마우스를 바라보는 방향이 된다.
      • 벡터 A - 벡터 B = B를 바라보는 A의 방향
  • 벡터 : 점을 가리키는 방향과 크기다.
    • x축이 0도이기 떄문에 x축으로부터 벡터의 각도가 라디안 각도다.
    • tan각도 = b / a
    • atan b/a = 각도

충돌 검사

  • Utils
    • CheckCollision : 사각형 2개가 충돌하는지 검사한다.
      • 사각형의 꼭짓점 좌표를 뽑고 polygonsIntersect() 함수로 검사한다.
      • 사각형, 스프라이트 등으로 만들 수 있다.
    • CheckCircleCollision : 원 충돌 검사
      • 두 원의 중심 거리와 반지름의 합을 비교해서 충돌여부를 판단한다.
      • 중심 거리^2 ≤ (반지름1 + 반지름2)^2
        • 중심 거리 : SqrMagnitude(중심 벡터A - 중심 벡터B)
        • 매개변수로 A의 중심벡터와 라디안 각도, B의 중심벡터와 라디안 각도를 받는다.
    • PointInTransformBounds : transformable 오브젝트의 localBounds를 전역 좌표로 반환하고, 특정 점이 이 안에 있는지 확인하는 함수
      • 마우스 클릭이 해당 오브젝트에 있는지 체크할 때 유용하다.
    • GetShapePoints( 사각형 또는 스프라이트) : 사각형 또는 스프라이트의 로컬 바운드로 꼭짓점 4개를 구하는 함수
    • GetRectanglePointsFromBounds : 꼭짓점의 위치를 저장한다.
      • 좌상단 - 우상단 - 우하단 - 좌하단 순서다.
    • PolygonsIntersect : 다각형 충돌 검사 함수
      • 각 도형 변에 수직 벡터 생성 - 검사용 축
      • 각 도형 해당 축에 정사영하고 최소 최대 비교
      • 축 하나라도 두 도형의 투영이 겹치지 않으면 충돌 안했다고 판단
      • 모든 축에서 겹치면 충돌한것이다.
    기능 대표 함수
    원 안/원 위 무작위 위치 RandomInUnitCircle, RandomOnUnitCircle
    각도 계산 및 변환 Angle, RadianToDegree, AngleRadian 등
    벡터 내적 계산 Dot
    충돌 검사 CheckCollision, CheckCircleCollision, PolygonsIntersect
    마우스/점 위치 검사 PointInTransformBounds
    회전/스프라이트 형태의 정밀 충돌 PolygonsIntersect + transform 조합

sf::View

  • https://www.sfml-dev.org/tutorials/2.6/graphics-view.php
  • 카메라가 이동하는 방법 정의 클래스다.
  • 중요한 멤버
    • Size : 뷰로 보여지는 화면 사이즈
      • 화면 사이즈보다 작게 설정하면 화면을 ‘줌’한 효과가 나온다.
    • Position : 카메라의 위치
      • 뷰의 중심 : 화면의 중앙
      • 뷰의 위치를 이동하면 카메라가 이동하는 효과를 일으킨다.
        • 이렇게 되면 플레이어같이 월드 좌표계의 좌표는 바뀌지 않지만, 마우스의 좌표는 보여지는 화면의 좌상단을 기준으로 표현되게 되어서 마우스의 좌표가 바뀌게된다.
        • 플레이어 : 뷰가 움직이는것이지 플레이어가 이동하게 된것이 아니다.
        • 결론 : 월드좌표계 → 스크린 좌표계 / 스크린 좌표계 → 월드 좌표계 를 변환하는 과정이 필요하다.
          • 스크린 좌표계 / 월드 좌표계 / UI 좌표계 가 필요한 것이다.
        • 월드 뷰와 UI 뷰를 따로 만들면 이동을 따로 해도 괜찮아진다.
  • 앞으로 씬을 만들게 되면 월드뷰와 ui뷰를 초기화 해줘야한다.

뷰 설명

  • 카메라 위치, 크기를 담당하는 클래스다.
  • setSize : 월드 위치에서의 사이즈
    • 정하면 해당 사이즈의 중심이 뷰의 중심이다.
  • WorldView
    • 카메라에 의해 비춰지는 게임 화면
  • UiView
    • UI 배치는 UI 좌표계를 기준으로 배치하게 된다.
sf::View uiView; //ui 보여주는 화면
sf::View worldView; //게임 씬 보여주는 화면
  • 벡터의 계산은 같은 좌표계에 있을때만 가능하다.
    • 같은 좌표계에 있지 않다면 함수를 이용해서 좌표계를 이동해서 좌표계산을 해야한다.
  • Ui좌표계를 월드좌표와 따로 둔 이유 : 카메라가 이동할때마다 UI에 속한 오브젝트 전부 좌표이동 시켜줘야하기 때문이다.
  • std::list<GameObject*> sortedObjects(gameObjects); sortedObjects.sort(DrawOrderComparer()); window.setView(worldView); //월드뷰에 그려지는 오브젝트는 월드뷰 좌표계를 기준으로 그려지게 된다. bool isUiViww = false; for (auto obj : sortedObjects) { if (obj->sortingLayer >= SortingLayers::UI && !isUiViww) { window.setView(uiView); isUiViww = true; } if (obj->GetActive()) { obj->Draw(window); } }
  • UI 레이어보다 작은것은 월드 좌표계를 기준으로 출력되는 것이다.
  • Ui 레이어보다 크다면 ? : UI뷰 세팅하고 isUiView true 한 다음 그 뒤 오브젝트는 UiView에 그려지게 된다.
//스크린 좌표계는 int 형이다.
sf::Vector2f ScreenToWorld(sf::Vector2i screenPos); //게임 화명에서 스크린 좌표가 어디를 가리키는가를 설정할때 사용
sf::Vector2i WorldToScreen(sf::Vector2f worldPos); //오브젝트의 윌드 위치를 화면에서 어느 픽셀에 그려야할때 사용
sf::Vector2f ScreenToUi(sf::Vector2i screenPos);
sf::Vector2i UiToScreen(sf::Vector2f uiPos);
  • 마우스를 바라보려면 스크린의 좌표를 월드좌표로 면환시켜야한다.
    • ScreenToWorld함수 사용

카메라 플레이어 따라가기

worldView.setCenter(player->GetPosition());
  • 뷰의 중앙을 플레이어의 위치로 설정하여 카메라가 플레이어를 따라가는 것처럼 설정한다.
  • 화면의 중앙에 플레이어가 고정되게 된다.

타일맵

  • https://www.sfml-dev.org/tutorials/2.6/graphics-vertex-array.php
  • vertex array
    • vertex : 정점
    • 정점 3개 → 삼각형
    • 삼각형 3개 → 3D 객체
    • 정점 3개가 모인 삼각형들이 모인 객체 : 입체
  • 지금은 Background Sheet를 4등분해서 사용할 것이다.
    • 스프라이트 : 통짜 이미지
    • 이것을 분할해서 특정 부분만 그릴 수 있다.
    • 큰 이미지에 스프라이트를 부분부분 넣고, 텍스처를 지금처럼 분할해서 쓰는것이 좋다.
      • 이유 : 스프라이트 이미지가 많아지면 용량이 커지기 때문이다.
      • 한 스프라이트 이미지를 쓰는것이 좋다.

vertex

  • sf::Points : 연결 안된 점
  • sf::Lines : 연결 안된 선들의 집합
  • sf::TriangleStrip : 연결된 삼각형들의 집합
    • 각 삼각형은 마지막 두 꼭짓점을 다음 삼각형과 공유한다.
    • 0 - 1 - 2 → 1 - 2 - 3으로 연결된다.
  • 다각형, 사각형도 같다.
  • sf::VertexArray.texCoords : 텍스처의 어떤 픽셀이 점점에 매핑되는지 정의하는 멤버다.
  • Background = x : 50, y : 200
    • 스프라이트 1 : (0, 0) - (50, 0) - (50, 50) - (0, 50) 으로 꼭짓점이 정해진다.
for (int i = 0; i < count.y; ++i)
{
	//셀 사이즈 x 순회
	for (int j = 0; j < count.x; ++j)
	{
		//내부는 랜덤 텍스처 출력
		int texIndex = Utils::RandomRange(0, 3); //0 ~ 2중 랜덤

		//외곽에 있는 셀이라면
		if (i == 0 || i == count.y - 1 || j == 0 || j == count.x - 1)
		{
			texIndex = 3; //외곽일 경우 3번 텍스처(벽돌만 있는 텍스처)로 설정한다.
		}

		//2차원 좌표를 1차원 좌표로 변환하는 식
		int quadIndex = i * count.x + j;
		//사각형의 첫번째 포지션
		sf::Vector2f quadPos(j * size.x, i * size.y); 

		for (int k = 0; k < 4; ++k)
		{
			//quadIndex * 4 : 사각형의 꼭짓점 / k : 꼭짓점 순서 (0 ~ 3)
			int vertexIndex = quadIndex * 4 + k; //해당 타일
			va[vertexIndex].position = quadPos + posOffset[k];
			va[vertexIndex].texCoords = texCoords[k];
			va[vertexIndex].texCoords.y += texIndex * 50.f;
		}
	}
}
  • 행과 열을 카운트(스프라이트 갯수)만큼 반복하여 좌표와 텍스처를 설정하는 코드다.
  • count.y : 행 / count.x : 열 이다.
  • int quadIndex = i * count.x + j; → 타일 인덱스 번호를 알기 위해 하는것이다.
    • ex) count.x = 5, i = 2, j = 3 → 가로갯수 5줄 , i는 2행, j는 3열이니 13번째 타일을 가리키게 된다.
  • quadPos : 현재 타일의 화면 좌표 상 위치
  • quadIndex * 4 + k : 현재 타일 꼭짓점 배열 인덱스
    • quadPos + posOffset[k] : 현재 타일 위치 + 꼭짓점 상대 위치
  • texCoords[k] : 텍스처 이미지 기본 정점 위치
    • .texCoords.y += texIndex * 50.f : y축 방향에서 타일의 행 위치다.
    • 하는 이유 : 세로로 긴 스프라이트를 자르기 위해서다.

Transform (변환)

  • SRT
    • scale
    • rotation
    • translate
    • 각 벡터를 새로운 벡터로 바꿔주는 행렬을 정의하여 생성하는 클래스다.
      • 기존 벡터와 Transform 정의한 행렬을 곱하여 변환된 벡터를 반환하는 클래스다.
  • 스케일 변환 - 회전 변환 - 이동 변환 순서로 변환이 진행된다.
    • 결합 법칙이 적용되기에 어떤것이 먼저 변환되어도 결과는 같다.

🔹 멤버 변수들

sf::VertexArray va;
std::string spriteSheetId = "graphics/background_sheet.png";
sf::Texture* texture = nullptr;
sf::Transform transform;

sf::Vector2i cellCount;
sf::Vector2f cellSize;
  • va: 정점 배열로, 타일맵의 모든 타일(사각형)을 저장합니다. 하나의 타일은 4개의 정점으로 표현됩니다.
  • spriteSheetId: 사용할 텍스처 시트 경로입니다.
  • texture: 실제 텍스처. 스프라이트 시트에서 각 타일의 이미지를 잘라서 사용합니다.
  • transform: 위치, 회전, 스케일, 기준점을 조합한 변환 행렬입니다.
  • cellCount: 맵의 가로(x), 세로(y) 셀 수.
  • cellSize: 각 셀의 크기 (픽셀 단위).

🔹 생성자

TileMap(const std::string& name = ""
  • 기본 생성자이며, 부모 클래스인 GameObject에 이름을 넘겨줍니다.

🔹 Set() 함수

void TileMap::Set(const sf::Vector2i& count, const sf::Vector2f& size)

목적: 타일 수와 각 타일 크기를 설정하고, 정점 배열을 구성합니다.

  1. cellCount, cellSize를 설정합니다.
  2. va.clear()로 기존 정점 초기화.
  3. va.setPrimitiveType(sf::Quads) → 사각형 단위로 그린다는 뜻입니다.
  4. va.resize(count.x * count.y * 4) → 타일 수 × 4(꼭짓점).
  5. posOffset은 한 타일의 네 꼭짓점 위치.
  6. texCoords는 텍스처 좌표입니다. 한 타일은 50×50 크기로 설정된 텍스처를 기준으로 합니다.
int texIndex = Utils::RandomRange(0, 3); // 랜덤 타일 선택
  • 맵 내부는 랜덤 텍스처를 사용하고,
if (i == 0 || i == count.y - 1 || j == 0 || j == count.x - 1)
    texIndex = 3; // 외곽은 벽돌 텍스처

  • 맵 테두리는 벽돌 타일로 고정합니다.
int vertexIndex = quadIndex * 4 + k;
va[vertexIndex].position = quadPos + posOffset[k];
va[vertexIndex].texCoords = texCoords[k];
va[vertexIndex].texCoords.y += texIndex * 50.f;

  • 하나의 타일을 구성하는 정점 4개를 각각 위치와 텍스처 좌표로 설정합니다.
  • texCoords.y에 texIndex * 50을 더해, 타일이 시트의 몇 번째 행에 있는지 조절합니다.

🔹 변환 관련 함수들

모두 부모 클래스의 값을 설정하고 UpdateTransform() 호출합니다.

void TileMap::UpdateTransform()
  • transform을 Identity로 초기화 → 기본 상태
  • translate(position), rotate(rotation), scale(scale)을 적용
  • 마지막에 translate(-origin) → 기준점을 중심으로 회전·스케일 조절하기 위함

🔹 SetOrigin(Origins preset)

  • Origins는 아마도 enum 타입으로, 기준점을 좌상단/중앙/우하단 등으로 설정하는 기능일 것 같아요.
origin.x = rect.width * ((int)preset % 3) * 0.5f;
origin.y = rect.height * ((int)preset / 3) * 0.5f;
  • 예: preset == MC (Middle Center)면 (1,1) → 가운데를 기준점으로 설정

🔹 Init(), Reset(), Release()

void TileMap::Init()

  • 맵을 초기화합니다.
  • 50×50 크기의 맵, 각 셀은 50x50 크기.
void TileMap::Reset()
  • 텍스처 로딩, 원점 중앙, 회전 45도, 위치 0으로 초기화
void TileMap::Release()
  • 아직 구현된 내용 없음 (자원 해제를 여기에 작성할 수 있음).

🔹 Draw()

void TileMap::Draw(sf::RenderWindow& window)
  • va를 window.draw()로 직접 그립니다.
  • RenderStates에 텍스처와 변환을 설정하여 정점 배열에 적용합니다.

✅ 총정리

구성 요소 설명

TileMap 타일맵을 정의하는 클래스
va 타일 정보를 담은 정점 배열
texture 텍스처 시트
transform 위치, 회전, 스케일, 기준점 조합
Set() 타일 배열 생성
Draw() 실제 화면에 타일맵 그리기
Reset() 초기값 설정 (중앙 기준, 회전 포함)

좀비 추가

방법 1 : 플레이 중 동적 추가 및 삭제

void SceneGame::Exit()
{
	for (Zombie* zombie : zombieList)
	{
		RemoveGameObject(zombie);
	}
	zombieList.clear();

	Scene::Exit();
}

void SceneGame::SpawnZombies(int count)
{
	//실행중에 게임오브젝트 추가
	for (int i = 0; i < count; ++i)
	{
		//Init을 거치면 Init과 Reset 호출을 안해도 자동이지만, Update는 Init 이후에 작동하기에 직접 호출해줘야한다.
		//문제점 : 씬 나갔다가 들어올때 남아있게 된다. -> 씬을 나갈때 동적할당한 객체는 delete를 해줘야한다.
		Zombie* zombie = (Zombie*)AddGameObject(new Zombie());
		zombie->Init();
		zombie->SetType((Zombie::Types)Utils::RandomRange(0, Zombie::TotalTypes)); //좀비 타입 랜덤 할당
		zombie->Reset();

		//Utils::RandomInUnitCircle() * 500.f : 반경 500 원 안에서 점 랜덤생성
		zombie->SetPosition(Utils::RandomInUnitCircle() * 500.f);

		zombieList.push_back(zombie); //리스트에 좀비 객체 추가
	}
}
  • 플레이중에 new 해준다면 Init과 Reset을 해줘야한다.
  • 플레이중에 new 했다면 씬을 나가게 될때 Remove를 해줘야한다.
  • 죽인 객체는 Remove하고 List에서 Remove를 해줘야 동적으로 했을때 안쓰는 객체를 삭제하고 다시 추가할 수 있다.
  • 필요할때 new, 안쓸때 delete
  • 문제점 : 플레이중 생성 및 삭제는 프로그램에 치명적으로 다가올 수 있다.
  • 비추천한다.

방법 2 : 오브젝트 비활성화 후 재사용

  • 처음에 많이 new 해 놓은 다음 list에 담아놓고 비활성화 시킨다.
    • new 할 타이밍에 비활성화한 객체를 다시 활성화 하는 것이다.
    • delete할 타이밍에 비활성화 한다.
  • 이것을 ‘**오브젝트 풀링’**하는 것이다.
  • 애매한 점 : new 할 갯수를 정해줘야하는데 몇개를 만들어야하는지 모르게된다.
    • 일단 일정한 갯수를 정해주고 모자랄때 new 를 해주는 ㅂ장식으로 사용한다.
for (int i = 0; i < 100; ++i)
{
	Zombie* zombie = (Zombie*)AddGameObject(new Zombie());
	zombie->SetActive(false);
	zombiePool.push_back(zombie);
} //Init 부분

void SceneGame::Exit()
{
	for (Zombie* zombie : zombieList)
	{
		zombie->SetActive(false);
		zombiePool.push_back(zombie);
	}
	zombieList.clear();

	Scene::Exit();
}

void SceneGame::SpawnZombies(int count)
{
	//실행중에 게임오브젝트 추가
	for (int i = 0; i < count; ++i)
	{
		Zombie* zombie = nullptr;
		//비활설화 된 좀비가 없을 경우
		if (zombiePool.empty())
		{
			//좀비 추가
			zombie = (Zombie*)AddGameObject(new Zombie());
			zombie->Init();
		}
		else
		{
			//비활성화 된 좀비가 있을 경우
			zombie = zombiePool.front(); //맨 앞 객체 꺼내서 저장
			zombiePool.pop_front(); //맨 앞부터 하나씩 제거
			zombie->SetActive(true); //활성화
		}
		
		zombie->SetType((Zombie::Types)Utils::RandomRange(0, Zombie::TotalTypes)); //좀비 타입 랜덤 할당
		zombie->Reset();
		//Utils::RandomInUnitCircle() * 500.f : 반경 500 원 안에서 점 랜덤생성
		zombie->SetPosition(Utils::RandomInUnitCircle() * 500.f);

		zombieList.push_back(zombie); //리스트에 좀비 객체 추가
	}
}

좀비 정지시키기

void Zombie::Update(float dt)
{
	if (Utils::Distance(player->GetPosition(), GetPosition()) >= 10.f)
	{
		//플레이어 쫒아다니기
		direction = Utils::GetNormal(player->GetPosition() - GetPosition()); //플레이어를 바라보는 방향 정규화
		SetRotation(Utils::Angle(direction)); //바라보는 방향까지의 각도 반환후 그 각도로 회전
		SetPosition(GetPosition() + direction * speed * dt);
	}
}
  • 거리를 재서 내가 정한 거리만큼 가까워지면 speed를 0으로 만든다.

마우스를 크로스헤어로 변경

  • 원래 커서 감추기