Game Programming in C++ - Day 5

이응민·2024년 7월 19일
0

Game Programming in C++

목록 보기
5/21

Day5 스프라이트

스프라이트(sprite)는 캐릭터나 배경 그리고 기타 동적인 물체를 나타내는데 사용되는 일반적인 2D게임상의 시작적 오브젝트이다. 2D 게임에서는 스프라이트가 널리 보급돼 사용되고 있으므로 가능한 한 효율적으로 스프라이트를 사용하는 것이 중요하다.

이미지 파일 로딩

2D 그래픽만 사용하는 게임에서 이미지 파일을 로드하는 간단한 방법은 SDL 이미지 라이브러리를 사용하는 것이다. 이미지 로드를 위한 첫 번째 단계로 요청된 파일 포맷의 플래그 파라미터를 인자로 받는 IMG_Init 함수를 이용해서 SDL Image를 초기화한다. 예를 들어 PNG 파일을 지원하고자 다음 호출을 GAME::Initialize에 추가한다.

IMG_Init(IMG_INIT_PNG);

SDL에서는 JPEG(IMG_INIT_JPG), PNG(IMG_INIT_PNG), TIFF(IMG_INIT_TIFF) 그리고 기본적으로 BMP 파일을 지원한다. SDL Image가 초기화되면 SDL_surface상으로 이미지 파일을 로드하고자 IMG_Load 함수를 사용한다.

// 파일로부터 이미지 로드
// 성공하면 SDL_Surface 포인터를 리턴하고 실패하면 nullptr을 반환
SLD_Surface* IMG_Load(
	const char* file // 이미지 파일 이름
);

다음으로 SDL_CreateTextureFromSurface 함수는 SDL_Surface를 SDL 화면에 그리는 데 필요한 포맷인 SDL_Texture로 변환한다.

// SDL_Surface를 SDL_Texture로 변환
// 성공하면 SDL_Texture 포인터를 리턴하고 실패하면 nullptr를 반환
SDL_Texture* SDL_CreateTextureFromSurface(
	SDL_Renderer* renderer, // 사용할 렌더러
    SDL_Surface* Surface // 변환될 SLD_Surface
);

다음 함수는 이미지 로딩 과정을 캡슐화한다.

SDL_Texture* LoadTexture(const char* fileName) {
	// 파일로부터 로딩
    SDL_Surface* surf = IMG_Load(fileName);
    if (!surf) {
    	SDL_Log("Failed to load texture file %s", fuelName);
        return Nullptr;
    }
    // 텍스터 생성
    SDL_Texture* text = SDL_CreateTextureFromSurface(mRnderer, surf);
    SDL_FreeSurface(surf);
    if (!text) {
    	SDL_Log("Failed to convert surface to texture for %s", fileName);
        return nullptr;
    }
    return text;

로드된 텍스터를 어디에 저장하는 것이 좋을까? 여러 액터에서 같은 이미지 파일을 사용하는 것은 게임에서 매우 일반적이다. 그러나 그 액터들이 같은 이미지 파일을 사용한다면 그 이미지 파일을 여러번 로드하는 것은 비효율적이다. 그래서 이미지 데이터는 여러 액터가 공유해서 사용하므로 싱글턴 클래서에 가까운 Game 클래스에 파일 이름과 SDL_Texture 포인터를 쌍으로한 맵을 만들어두면 좋을 것이다. 그 다음 텍스처의 이름을 인자로 받고 해당 이름과 일치하는 SDL_Texture 퍼인터를 반환하는 GetTexture 함수를 만들자. 이 함수는 처음에 텍스처가 이미 맵에 존재하는지를 확인한다. 맵에 존재한다면 텍스처 포인터를 리턴한다. 그렇지 않으면 파일로부터 텍스처를 로드하는 코드를 실행한다.그리고 Game에 LoadData 함수를 구현한다. 이 함수는 게임 세계의 모든 액터를 생성할 책임을 가진다. Game::Initialize의 마지막에 LoadData 함수 호출을 추가한다.

스프라이트 그리기

게임에 기본 2D 장면인 배경 이미지와 캐릭터가 있다고 하면 이 장면을 그리기 위해 먼저 배경을 먼저 그리고 캐릭터를 그리는 것이 간단한 방법일 것이다. 이 순서는 화가가 장면을 그리는 것과 같다. 그래서 이러한 접근법은 화가 알고리즘(painter's algorithm)으로 알려져 있다. 스프라이트를 그리기 위해 스프라이트에 관련된 컴포넌트(SpriteComponent)를 만들어준다.

SpriteComponent.h

SpriteComponent.cpp

게임은 mDrawOrder 멤버 변수에 지정된 순서로 스프라이트 컴포넌트를 그려서 화가 알고리즘을 구현한다. SpriteComponent 생성자는 Game::AddSprite 함수를 호출해서 Game 클래스의 스프라리트 컴포넌트 벡터(std::vector<class SpriteComponent*> mSprites)에 자신을 추가한다. Game::AddSprite에서 mSprite는 그리는 순서로 정렬되야한다. AddSprite를 호출할 때 mSpriteㅇ는 이미 정렬된 순서를 유지하고 있으므로 이미 정렬된 벡터에 스프라이트를 넣음으로써 정렬을 구현할 수 있다. 결론적으로 AddSprite 함수는 sprite를 mSprites 벡터에 정렬해서 넣는 역할을 한다.

스프라이트 컴포넌트는 mDrawOrder순으로 이미 정렬됐으므로 Game::GenerateOutput에서는 스프라이트 컴포넌트 벡터를 반복하면서 각 스프라이트 컴포넌트의 Draw 함수만 호출하면 된다. 이 코드를 후면 버퍼를 클리어하는 코드 뒤쪽 그리고 후면 버퍼와 전면 버퍼를 스왑하는 코드 앞에 추가하면 된다.

SetTexture 함수는 mTexture 멤버 변수를 설정하며, SDL_QueryTexture를 사용하면 텍스처의 너비와 높이를 얻는 것이 가능하다.

텍스처를 그리기 위해 SDL에서는 2가지의 텍스처 그리기 함수를 제공한다. 간단한 함수는 SDL_RenderCopy 함수다.

// 텍스처를 렌더링 타겟에 그린다
// 성공하면 0을 반환하고 실패하면 음수를 반환한다
int SDL_RenderCopy(
	SDL_Renderer* renderer, // 그려질 렌터 타겟
    SDL_Texture* texture, // 그릴 텍스처
   	const SDL_Rect* srcrect, // 그릴 텍스처의 일부 영역 (전체 영역이면 nullptr)
    const SDL_Rect* dstrect, // 타겟에 그릴 사각형 영역
);

스프라이트를 회전하는 것과 같은 고급 기능을 이용하려면 SDL_RenderCopyEx를 사용해야한다.

// 텍스처를 렌더링 타겟에 그린다
// 성공하면 0을 반환하고 실패하면 음수를 반환한다
int SDL_RenderCopyEx(
	SDL_Renderer* renderer, // 그려질 렌터 타겟
    SDL_Texture* texture, // 그릴 텍스처
   	const SDL_Rect* srcrect, // 그릴 텍스처의 일부 영역 (전체 영역이면 nullptr)
    const SDL_Rect* dstrect, // 타겟에 그릴 사각형 영역
    double angle, // 회전 각도 (각도(degree), 시계 방향)
    const SDL_Point* center, // 회전 중심점 (중심이면 nullptr)
    SDL_RenderFlip flip, (텍스처를 플립하는 방법 (대개 SDL_FLIP_NONE)
);

액터는 회전값(mRotation)을 갖고있는데 스프라이트가 이 회전을 물려받아야한다면 SDL_RenderCopyEx를 사용해야한다. 이 때문에 SpriteComponent::Draw 함수는 복잡성이 약간 증가한다. 먼저, SDL_Rect 구조체에 x,y좌표는 이미지 상단 왼쪽 모서리에 해당하는데, 액터의 중심점이므로 퐁 게임의 공과 패들처럼 액터의 왼쪽 상단 모서리의 좌푯값을 계산해야 한다. 두 번째로 SDL은 도(degree) 단위의 각도를 원하는데, 액터는 라디안 값을 사용한다. 그래서 라디안 값을 도 단위로 변환해줘야한다. 마지막으로 SDL의 양의 각도는 시계방향인데, 이 방향은 단위 원(양의 각도는 반시계 방향)과 반대이다. 그러므로 단위 원의 동작은 변경하지 않고 SDL에 각도를 전달해주기 위해 각도를 반전 시킨다.

Draw의 구현에서는 액터의 위치가 화면상의 액터와 일치한다고 추정한다. 이 추정은 게임 세계가 정확히 화면과 일치하는 게임에서만 유효하며 슈퍼 마리오와 같은 단일 화면보다 훨씬 더 큰 게임 세계를 갖고 있는 게임에서는 카메라의 위치도 필요하다.

스프라이트 애니메이션

대부분의 2D 게임은 플립북 애니메이션(flipbook animation)같은 테크닉을 사용해서 스프라이트 애니메이션을 구현한다. 플립북 애니메이션은 빠르고 연속적인 일련의 2D 이미지를 넘겨서 움직임의 환영을 만들어내는 애니메이션 기법을 의미한다. 스프라이트 애니메이션의 프레임 레이트는 다양할 수 있지만, 많은 게임에서는 24FPS의 사용을 선택한다. 이는 애니메이션이 매 초당 24개의 개별적인 이미지가 필요함을 뜻한다. 스프라이트 애니메이션을 재생하는 가장 간단한 방법은 애니메이션을 위해 각 프레임과 일치하는 여러 이미지들의 벡터를 가지는 것이다. 따라서 AnimSpriteComponent 클래스를 선언해서 이 접근법을 사용한다.

AnimSpriteComponent.h

AnimSpriteComponent.cpp

mAnimFPS 변수를 사용하면 애니메이션이 가능한 스프라이트를 다른 프레임 레이트로 실행할 수 있다. 또한 이 변수를 사용하면 애니메이션 재생 속도를 동적으로 증가시키거나 낮추는 것이 가능하다. 예를 들어 캐릭터의 속도가 빨라지면 캐릭터가 빨리는 것처럼 보이기 위해 애니메이션의 프레임 레이트를 증가시키면 된다. 그리고 mCurrFrame 변수는 출력 중인 현재 프레임을 float값으로 기록하는데 이 값을 확인하면 해당 프레임이 시작된 후 경과된 시간을 알아내는 것이 가능하다. SetAnimTexture 함수는 mAnimTextures멤버 변수를 함수의 첫 번째 파라미터인 벡터값으로 설정하고 mCurrFrame 값을 0으로 초기화한다. 그리고 SetTexture 함수를 호출해서(SpriteComponent에서 상속받은 함수) 애니메이션의 첫 번째 프레임을 설정한다. AnimSpriteComponent는 SpriteComponent의 SetTexture 함수만 사용할 뿐 렌더링을 위한 특별한 변경 사항은 없으므로 상속한 Draw 함수를 재정의할 필요는 없다. Update 함수는 AnimSpriteComponent의 대부분의 작업이 일어나는 곳이다. 먼저, 애니메이션 FPS와 델타시간으로 mCurrFrame을 갱신한다. 다음으로, mCurrFrame이 텍스처의 수보다 작게 유지되는지를 확인한다. 이는 상황에 따라 애니메이션의 시작 지점으로 되돌아갈 수 있다는 것을 의미한다. 마지막으로, mCurrFrame을 int형으로 타입 캐스트한 후 mAnimTexture로부터 원하는 텍스처를 얻은 뒤 SetTexture를 호출한다. AnimSpriteComponent에서 빠진 한 가지 기능은 애니메이션 간 전환에 대한 제대로된 지원이다. 현재 상황에서 애니메이션을 전환하는 유일한 방법은 SetAnimTextures를 함수를 반복해서 호출하는 것이다.

0개의 댓글