SFML 3

김성진·2024년 2월 5일

2월 5일 수업 정리

Timber 게임을 계속해서 만들어본다.
이제 점점 본격적인 게임 메카닉에 관련된 부분을 작업할 것이다.

나뭇가지 내려오기

나무를 베면서 나뭇가지들이 랜덤하게 내려오는것을 피하는 게임이다.
따라서 나뭇가지들을 배치하고, 액션을 취할때마다 한칸씩 내려오게 만든다.
굳이 여러개 만들 필요 없고, 화면에 들어갈 5개 + 이후 들어올 한개 해서 6개만 만들어놓고 맨 아래 나뭇가지는 맨위로 돌려놓아 재사용하는 방식으로 만든다.

나뭇가지 상수

나뭇가지는 단순히 일렬로 내려오는것이 아닌, 왼쪽 오른쪽 그리고 없음의 3가지 종류로 내려온다. 착각하기 쉬운게, 나무 줄기에 나뭇가지가 없는 부분도 나뭇가지 배열에 들어간다. 단지 없는 상태로 저장될 뿐이다.
그럼 나뭇가지의 상태를 나타낼 열거형 Sides를 만들어보자.

enum class Sides
{
	LEFT, RIGHT, NONE
};

// 좌 우 없음  가지 상태 열거형

0 1 2 로 나타낸다.

나뭇가지 배치

나뭇가지 하나당 한개의 스프라이트가 필요하지만 모두 같은 역할을 할 것이니 배열을 사용하여 만든다.

void UpdateBranches();

const int NUM_OFBRANCHES = 6;

sf::Sprite branches[NUM_OFBRANCHES]; // 가지 생성 배열
Sides brancheSides[NUM_OFBRANCHES]; // 가지 방향의 상태를 맡는배열

가지 배열을 변경해줄 함수 UpdateBranches를 선언해놓고,
6개 칸을 가진 배열을 만들어 줄텐데 단순히 6이라 쓰지 않고 가지의 갯수 라는 식별자를 붙여 관리하도록 한다.
Sprite branches 는 가지 자체의 배열,
Sides brancheSides는 가지들의 상태를 저장하는 배열로 0 1 2로만 구성되어있다.

    for (int i = 0; i < NUM_OFBRANCHES; ++i)
    {
        window.draw(branches[i]);
    }

가지를 그려준다.
이후 가지의 포지션을 방향에 따라 설정해주는 코드를 작성해준다.

    float treeHarfWidth = spriteTree.getLocalBounds().width * 0.5f;
    float centerX = windowWidth * 0.5;

    for (int i = 0; i < NUM_OFBRANCHES; ++i)
    {
        float y = i * 150.f;
        switch(brancheSides[i])
        {
        case Sides::LEFT:
            branches[i].setScale(-1.f, 1.f);
            branches[i].setPosition(centerX - treeHarfWidth, y);
            break;
        case Sides::RIGHT:
            branches[i].setScale(1.f, 1.f);
            branches[i].setPosition(centerX + treeHarfWidth, y);
            break;
        default:
            branches[i].setPosition(-2000 , -2000);
            break;
        }
    }

treeHarfWidth 와 centerX 는 나무와 화면의 중앙값을 제공해주는 변수이다.
가지의 배열을 순회하며, 인덱스가 쌍을 이루는 가지상태 배열의 값에 맞춰 가지의 포지션과 방향을 결정해준다.

나뭇가지 상태 배열

나뭇가지 상태 배열값을 순차적으로 옮기고 새로운 값을 넣어주는 함수 UpdateBranches를 작성한다.

void UpdateBranches()
{
    for (int i = NUM_OFBRANCHES - 1; i > 0; --i)
    {
        brancheSides[i] = brancheSides[i - 1];
    }

    int side = rand() % 5;
        switch (side)
        {
        case 0:
            brancheSides[0] = Sides::LEFT;
            break;
        case 1:
            brancheSides[0] = Sides::RIGHT;
            break;
        default:
            brancheSides[0] = Sides::NONE;
            break;
        }
}

난수값을 5를 나눈 나머지로 산출해 좌 우에 각각 20%, 아무것도 없을 확률을 60%로 설정했다.

확률을 만드는 방법

   float value = (float)rand() / RAND_MAX; // 0.0~1.0
    if (value < 0.3)
    {}

0~1이 나오게 하고 그중에 0.3보다 적은것만 뽑으면 그게 30프로다. 이런 방법도 있다.

컨테이너 클래스, 배열과 링크드 리스트

다수의 객체나 자료를 보관하는 클래스를 컨테이너 클래스라고 한다. 여러 구조가 있지만 지금까지 배운건 배열 뿐이다. 이제 링크드 리스트를 알아보자.
리스트는 노드로 이루어져 있으며, 노드의 내용물은 자신의 데이터와 다음 노드의 주소값만 가지고있다. 이러한 노드들이 꼬리를 물고 엮여 있으며, 첫번째인 헤드 노드만 가지고 관리하는 것이다.
리스트의 장점은 앞이든 뒤든 중간이든 어떤 노드도 추가, 삭제하기 쉽다. 해당 노드와 그 앞 노드의 주소값만 변경하는 2번의 연산이면 된다. 헤드도 교체하기 쉽다.
대신 단점은 특정 값을 찾으려면 헤드부터 해서 모든 리스트를 순회해야 한다는 점이다. 한 노드의 주소는 그 앞 노드에밖에 적혀있지 않으니까.
배열의 장점인 어떤 랜덤위치든 즉시 액세스 가능한것과 단점인 길이변경 불가 등이 그대로 반전된 것이 리스트인 것이다.

인풋매니저

SFML에서 키 입력에 관한 함수는 KeyPressed 와 KeyReleased 밖에 없다. 문제는 프레스드는 눌려있을때 계속 true 값을 보내서 한번만 입력해야 되는 상황에서 여러번 입력받는다는 점이다. 따라서 키 입력을 관리하는 inputMgr 를 만들어 프레임워크에 넣고 두고두고 사용할 것이다.
우선 inputMgr 헤더파일과 소스파일을 만든다.

헤더
#pragma once
#include <SFML/Graphics.hpp>
#include <list>

class inputMgr
{
private:
	static std::list<sf::Keyboard::Key> downList;
	static std::list<sf::Keyboard::Key> upList;
	static std::list<sf::Keyboard::Key> ingList;

public:
	static void UpdateEvent(const sf::Event& ev); // 메인함수에서 이벤트를 받는 함수
    static void Clear();
    
	static bool GetKeyDown(sf::Keyboard::Key key); // 눌리는 순간만,
	static bool GetKeyUp(sf::Keyboard::Key key); // 떨어지는 순간만,
	static bool GetKey(sf::Keyboard::Key key); // 특정한 키가 눌려지고 있을때, 리턴

};

누른 키, 뗀 키, 누르고 있는 키를 각각 저장하는 세 리스트를 만들고, 여기에 값을 저장하게 해주는 함수를 세개 만든다. 또한 이벤트를 받는 UpdateEvent 함수, 받은 값을 초기화 해주는 Clear 함수도 만들어준다.

이제 구현해주면

bool inputMgr::GetKeyDown(sf::Keyboard::Key key)
{
	/*std::list<sf::Keyboard::Key>::iterator it = downList.begin(); *///iterator는 순회용 객체, ++ -- 도 재구현돼있어서 앞뒤이동가능
	//만일 end를 호출하면 맨 마지막이 아니라 그 뒤에 빈 노드 하나 나옴. 널문자 같은 역할
	// auto it = downList.begin();
	//for (auto it = downList.begin(); it != downList.end(); ++it)
	//{
	//	if (*it == key)  // 왜 포인터냐면 노드의 주소를 들고있으니까. 노드는 본인 값과 다음 노드 주소값을 가짐
	//	{
	//		return true;
	//	}
	//}

	//for (auto k : downList)
	//{
	//	if (k == key)
	//	return true;
	//}
	return std::find(downList.begin(), downList.end(), key) != downList.end();
}

주석으로 비슷한 결과를 내는 여러 방법이 있지만 가장 간단하게, 리스트의 시작부터 끝까지 검사해서 해당 키가 이미 저장되어 있으면 true를, 없으면 false를 리턴한다. 리스트의 end는 null과 같다.

bool inputMgr::GetKeyUp(sf::Keyboard::Key key)
{
	return std::find(upList.begin(), upList.end(), key) != upList.end();
}

bool inputMgr::GetKey(sf::Keyboard::Key key)
{
	return std::find(ingList.begin(), ingList.end(), key) != ingList.end();
}

나머지 함수도 똑같이 만들어준다.
이제 UpdateEvent과 Clear를 구현한다.

void inputMgr::UpdateEvent(const sf::Event& ev)
{
	switch (ev.type)
	{
	case sf::Event::KeyPressed:
		if (!GetKey(ev.key.code))
		{
			ingList.push_back(ev.key.code); // 리스트 맨 뒤에 넣는 함수
			downList.push_back(ev.key.code);
		}
		break;
	case sf::Event::KeyReleased:
		ingList.remove(ev.key.code); // 지워버리는 함수
		upList.push_back(ev.key.code);
		break;
	}
}

void inputMgr::Clear()
{
	downList.clear();
	upList.clear();
}

특정 이벤트를 체크하는 switch문을 만들어 이벤트 루프안에 넣고 매 호출마다 키가 눌렸는지 검사하도록 만들 것이다.
KeyPressed로 해당 키 입력이 들어왔을 때, 리스트 안에 없으면 False를 반환하니 !를 붙여서 리스트 안에 없다면 누르고 있는 리스트와, 눌린 리스트에 해당 키를 추가한다.
KeyReleased 로 뗄때는, 누르고 있는 리스트에선 제거하고 뗀 리스트에 추가한다.

이후 clear 를 통해 눌린 리스트 뗀 리스트를 초기화 해주면, 한번 누르면 떼기 전까지 누르고 있는 리스트에선 존재하지만 눌린 리스트에는 없어서 한번만 입력을 받게 된다.
이제 메인 루프 안에 작성해보자

        inputMgr::Clear();

        sf::Event event;
        while (window.pollEvent(event))
        {
        
            inputMgr::UpdateEvent(event);
            switch (event.type)
            {
            case sf::Event::Closed:
                window.close();
                break;
            }

매 호출마다 Clear를 해주고, 이벤트 입력을 검사한다. 윈도우창 닫는 이벤트도 저 함수안에 집어넣으면 좀더 깔끔해질 것이다.


        if (inputMgr::GetKeyDown(sf::Keyboard::Enter))
        {
            isPause = !isPause;
        }
        timeScale = isPause ? 0.f : 1.f;

        if (inputMgr::GetKeyDown(sf::Keyboard::Space))
        {
            UpdateBranches();
        }

이전에 만들었던 일시정지 기능, 점수기능 등등이랑 가지 재배열 기능 등을 배치해줬다.
이것들은 이제 이벤트 루프에서 빠져나와서 메인 루프안에 배치해야 한다.

profile
듀얼리스트

0개의 댓글