짧은 설명
- 브런치 이름으로 하는 대표적인 이름
- 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 : 내적
- 각도 비교 등에 사용된다.
- 기준 벡터 ~ 찾는 벡터까지의 각도를 알 수 있게 된다.
- 앞 ~ 뒤까지의 각도를 알 수 있다.
- 각도 비교 등에 사용된다.
- RadianToDegree : 라디안 각도를 디그리로 변환
- 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 조합 - CheckCollision : 사각형 2개가 충돌하는지 검사한다.
sf::View
- https://www.sfml-dev.org/tutorials/2.6/graphics-view.php
- 카메라가 이동하는 방법 정의 클래스다.
- 중요한 멤버
- Size : 뷰로 보여지는 화면 사이즈
- 화면 사이즈보다 작게 설정하면 화면을 ‘줌’한 효과가 나온다.
- Position : 카메라의 위치
- 뷰의 중심 : 화면의 중앙
- 뷰의 위치를 이동하면 카메라가 이동하는 효과를 일으킨다.
- 이렇게 되면 플레이어같이 월드 좌표계의 좌표는 바뀌지 않지만, 마우스의 좌표는 보여지는 화면의 좌상단을 기준으로 표현되게 되어서 마우스의 좌표가 바뀌게된다.
- 플레이어 : 뷰가 움직이는것이지 플레이어가 이동하게 된것이 아니다.
- 결론 : 월드좌표계 → 스크린 좌표계 / 스크린 좌표계 → 월드 좌표계 를 변환하는 과정이 필요하다.
- 스크린 좌표계 / 월드 좌표계 / UI 좌표계 가 필요한 것이다.
- 월드 뷰와 UI 뷰를 따로 만들면 이동을 따로 해도 괜찮아진다.
- Size : 뷰로 보여지는 화면 사이즈
- 앞으로 씬을 만들게 되면 월드뷰와 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)
목적: 타일 수와 각 타일 크기를 설정하고, 정점 배열을 구성합니다.
- cellCount, cellSize를 설정합니다.
- va.clear()로 기존 정점 초기화.
- va.setPrimitiveType(sf::Quads) → 사각형 단위로 그린다는 뜻입니다.
- va.resize(count.x * count.y * 4) → 타일 수 × 4(꼭짓점).
- posOffset은 한 타일의 네 꼭짓점 위치.
- 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으로 만든다.
마우스를 크로스헤어로 변경
- 원래 커서 감추기