728x90

1. 데이터 지역성 패턴 ( Data Locality Pattern ) 정의

  • CPU 캐시를 최대한 활용할 수 있도록 데이터를 배치해 메모리 접근 속도를 높인다.

 

2. 캐시 라인 ( Cache Line )

캐시 라인

  • RAM으로부터 데이터를 한 바이트라도 가져와야 할 경우, RAM은 ( 보통 64~128바이트 정도의 ) 연속된 메모리를 선택해 캐시에 복사한다. 이런 메모리 덩어리를 캐시 라인이라고 한다.
  • 필요한 데이터가 캐시 라인 안에 들어 있다면 CPU는 RAM보다 훨씬 빠른 캐시로부터 데이터를 가져올 수 있다.

 

3. 캐시 히트 ( Cache Hit ), 캐시 미스 ( Cache Miss )

  • 캐시에서 원하는 데이터를 찾는 것을 캐시 히트
  • 데이터를 찾지 못해 주 메모리에서 데이터를 가져오는 것을 캐시 미스라고 한다. ( 캐시 미스 발생 시 CPU는 멈춘다. )

 

4. 캐시 미스가 발생되는 코드

	while ( !bGameOver )
	{
		// AI를 처리한다.
		for ( int index = 0; index < numEntities; ++index )
			entities[ index ]->GetAI()->Update();

		// 물리를 업데이트한다.
		for ( int index = 0; index < numEntities; ++index )
			entities[ index ]->GetPhysics()->Update();

		// 화면에 그린다.
		for ( int index = 0; index < numEntities; ++index )
			entities[ index ]->GetRender()->Render();
	}
  • 게임 개체가 배열에 포인터로 저장되어 있어, 배열 값에 접근할 때마다 포인터를 따라가면서 캐시 미스가 발생한다.
  • 게임 개체는 컴포넌트를 포인터로 들고 있어서 다시 한번 캐시 미스가 발생한다.

 

5. 캐시 히트를 높히기 위한 코드

	// 컴포넌트 자료형별로 큰 배열 준비
	AIComponent* aiComponents = new AIComponent[ MAX_ENTITIES ];
	PhysicsComponent* physicsComponents = new PhysicsComponent[ MAX_ENTITIES ];
	RenderComponent* renderComponents = new RenderComponent[ MAX_ENTITIES ];

	while ( !bGameOver )
	{
		// AI를 처리한다.
		for ( int index = 0; index < numEntities; ++index )
			aiComponents[ index ].Update();

		// 물리를 업데이트한다.
		for ( int index = 0; index < numEntities; ++index )
			physicsComponents[ index ].Update();

		// 화면에 그린다.
		for ( int index = 0; index < numEntities; ++index )
			renderComponents[ index ].Render();
	}
  • 배열에 컴포넌트 포인터가 아닌, 컴포넌트 객체를 넣어 사용한다.
  • 모든 데이터가 배열 안에 나란히 들어 있기 때문에 게임 루프에서는 객체에 바로 접근할 수 있다.
  • 간접 참조 연산자 ( -> )의 개수를 줄여 포인터를 따라가며 발생하는 캐시 미스를 개선할 수 있다.
반응형
728x90

1. 서비스 중개자 패턴 정의

  • 서비스를 구현한 구체 클래스는 숨긴 채로 어디에서나 서비스에 접근할 수 있게 하는 패턴

 

2. 서비스 중개자 패턴 구현

// 오디오 서비스가 제공할 인터페이스.
class Audio
{
public:
	virtual ~Audio() {}
	virtual void PlaySound( int InSoundId ) = 0;
	virtual void StopSound( int InSoundId ) = 0;
	virtual void StopAllSounds() = 0;
};

// 오디오 서비스
class ConsoleAudio : public Audio
{
	virtual void PlaySound( int InSoundId )
	{
		// 콘솔의 오디오 API를 이용해 사운드를 출력한다.
	}

	virtual void StopSound( int InSoundId )
	{
		// 콘솔의 오디오 API를 이용해 사운드를 중지한다.
	}

	virtual void StopAllSounds()
	{
		// 콘솔의 오디오 API를 이용해 모든 사운드를 중지한다.
	}
};

// 서비스 중개자
class Locator
{
private:
	static Audio* Service;

public:
	// 중개 역할을 하는 GetAudio() 함수
	static Audio* GetAudio() { return Service; }

	static void Provide( Audio* InService ) { Service = InService; }
};

int main()
{
	// Locator가 오디오 서비스를 등록하는 방법
	ConsoleAudio* consoleAudio = new ConsoleAudio();
	Locator::Provide( consoleAudio );
	
	// 중개자를 통해 Audio 인스턴스를 반환받아 사용
	Audio* audio = Locator::GetAudio();
	audio->PlaySound( SOUND_1 );
}
  • 정적 함수인 GetAudio()가 중개 역할을 한다.
  • 어디에서나 부를 수 있는 GetAudio() 함수는 Audio 인스턴스를 반환해 사용할 수 있도록 한다.
  • PlaySound()를 호출하는 쪽에서는 Audio라는 추상 인터페이스만 알 뿐, ConsoleAudio라는 구체 클래스에 대해서는 전혀 모른다는 게 핵심이다.
  • Locator 클래스와 서비스 제공자의 구체 클래스와는 커플링 되지 않는다.
  • 어떤 구체 클래스가 실제로 사용되는지는 서비스를 제공하는 초기화 코드에서만 알 수 있다.

 

3. 널 객체 패턴 구현

// 널 객체 디자인 패턴
class NullAudio : public Audio
{
public:
	virtual void PlaySound( int InSoundId ) { /*아무것도 하지 않는다.*/ }

	virtual void StopSound( int InSoundId ) { /*아무것도 하지 않는다.*/ }

	virtual void StopAllSounds() { /*아무것도 하지 않는다.*/ }
};

// 중개자
class Locator
{
private:
	static Audio* Service;
	static NullAudio NullService;

public:
	// 널 서비스로 초기화
	static void Initialize()
	{
		Service = &NullService;
	}
	// 중개 역할을 하는 GetAudio() 함수
	static Audio* GetAudio() { return Service; }

	static void Provide( Audio* InService ) 
	{
		if ( Service == nullptr )
		{
			// 널 서비스로 돌려놓는다.
			Service = &NullService;
		}
		else
		{
			Service = InService;
		}
		Service = InService; 
	}
};
  • 서비스 제공자가 서비스를 등록하기 전에 서비스를 사용하려고 하면, NULL을 반환해 크래시 위험이 있다.
  • 객체를 찾지 못하거나 만들지 못해 NULL을 반환해야 할 때, 대신 같은 인터페이스를 구현한 특수한 객체를 반환한다.
  • 아무런 구현이 되어 있지 않지만, 객체를 받는 쪽에서는 '진짜' 객체를 받은 것처럼 안전하게 작업을 할 수 있다.

 

4. 데커레이터 패턴 구현

// 데커레이터 패턴
class LoggedAudio : public Audio
{
private:
    Audio& Wrapped;

private:
    void Log( cosnt std::string& InMessage ) 
    {
        // 로그
    }

public:
    LoggedAudio( AUdio& InWrapped ) : Wrapped( InWrapped ) {}

    virtual void PlaySound( int InSoundId )
    {
        log ( "사운드 출력" );
        Wrapped.PlaySound( InSoundId );
    }

	virtual void StopSound( int InSoundId )
    {
        log ( "사운드 중지" );
        Wrapped.StopSound( InSoundId );
    }

	virtual void StopAllSounds()
    {
        log ( "모든 사운드 중지" );
        Wrapped.StopAllSounds();
    }
};

int main()
{   
    // 기존 서비스를 데커레이트한다.
    Audio* service = new LoggedAudio( Locator::GetAudio() );

    // 이 값으로 바꿔치기한다.
    Locator::Provide( service );
}
  • 조건적으로 로그를 남기고 싶은 시스템이 서비스로 노출되어 있다면 데커레이터 패턴 ( 장식자 패턴 )으로 해결이 가능하다.
  • LoggedAudio 클래스는 다른 오디오 서비스 제공자를 래핑하는 동시에, 같은 인터페이스를 상속받는다.
  • 실제 기능 요청은 내부에 있는 서비스에 전달하고, 대신 사운드가 호출될 때마다 로그를 남긴다.

 

반응형
728x90

1. 이벤트 큐 패턴 정의

  • 메시지나 이벤트를 보내는 시점과 처리하는 시점을 디커플링 하는 패턴
  • 게임 시스템들이 디커플링 상태를 유지한 채로 서로 고수준 통신을 하고 싶을 때 사용한다.

 

2. 원형 버퍼를 이용한 이벤트 큐 패턴 구현

struct PlayMessage
{
    SoundId Id;
    int Volume;
};

class Audio
{
private:
    static int Head;
    static int Tail;

    static const int MAX_PENDING = 16;
    static PlayMessage Pending[ MAX_PENDING ];

public:
    static void Init()
    {
        Head = 0;
        Tail = 0;
    }

     static void PlaySound( SoundId InId, int InVolume )
     {
        // 동일한 소리의 경우, 파장이 커지는 문제가 있어 요청 취합
        // 보류 중인 요청을 쭉 살펴본다.
        for ( int index = Head; index != Tail; index = ( index + 1) % MAX_PENDING )
        {
            if ( Pending[ index ].Id != InId )
                continue;

            // 둘 중 소리가 큰 값으로 덮어쓴 뒤, 큐에 넣지 않는다.
            Pending[ index ].Volume = max( InVolume, Pending[ index ].Volume );
            return;
        }

        // Tail값이 Head를 덮는 경우를 방지한다.
        if ( ( Tail + 1 ) % MAX_PENDING == Head )
            return;

        Pending[ Tail ].Id  = InId;
        Pending[ Tail ].Volume = InVolume;

        //원형 버퍼를 위해 Tail 조정
        Tail = ( Tail + 1 ) % MAX_PENDING;
     }


     static void Update()
     {
        // 보류된 요청이 없다면 아무것도 하지 않는다.
        if ( Head == Tail )
            return;
        
        ResourceId resourceId = LoadSound( Pending[ Head ].Id );
        int channel = FindOpenChannel();
        if ( channel == -1 )
            return;

        StartSound( resource, channel, Pending[ index ].Volume );
        
        //원형 버퍼를 위해 Head 조정
        Head = ( Head + 1 ) % MAX_PENDING;
     }
};
  • 요청 시점과, 처리 시점을 분리하여 게임 루프 혹은 별도 오디오 스레드에서 Update()를 호출하도록 구현
  • 요청 취합은 요청을 처리하는 시점 Update()에서 진행할 수 도 있다.

 

반응형
728x90

1. 컴포넌트 패턴 정의

  • 한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있게 한다.

 

2. 커플링 된 함수

if ( CollidingWithFloor() && ( GetRenderState() != ERanderState::INVISIBLE ) )
{
    PlaySound( ESound::HIT_FLOOR );
}
  • 물리 ( CollidingWithFloor() ), 그래픽 ( GetRenderState() ), 사운드 ( PlaySound() )가 커플링 된 소스 코드
  • 소스 코드 수정을 위해서는 위 3가지 항목을 전부 알아야 하기에 유지보수가 힘들다.

3. 커플링 된 클래스

class GameObject
{
private:
    static const int WALK_ACCELERATION = 1;

    int Velocity;
    int X, Y;

    FVolume Volume;

    FSprite SpriteStand;
    FSprite SpriteWalkLeft;
    FSprite SpriteWalkRight;

public:
    GameObject() : Velocity( 0 ), X ( 0 ), Y ( 0 ){}
    void Update( FWorld& InWorld, FGraphics& InGraphics )
    {
        // 입력에 따라 주인공의 속도를 조절한다.
        switch ( Controller::GetJoystickDirection() )
        {
            case Direction::Left:
            Velocity -= WALK_ACCELERATION;
            break;

            case Direction::Right:
            Velocity += WALK_ACCELERATION;
            break;
        }

        // 속도에 따라 위치를 바꾼다.
        X += Velocity;
        InWorld.ResolveCollision( Volume, X, Y, Velocity );

        // 알맞은 스프라이트를 그린다
        FSprite* sprite &SpriteStand;
        if ( Velocity < 0 )
            sprite = &SpriteWalkLeft;
        else if ( Velocity > 0 )
            sprite = &SpriteWalkRight;
        
        InGraphics.Draw( *sprite, X, Y );
    }
};
  • Update 함수 안에 물리엔진 / 그래픽 엔진 등 함수들이 커플링 되어 있다.

 

3. 분야별로 컴포넌트화

class InputComponent
{
private:
    static const int WALK_ACCELERATION = 1;

public:
    void Update( GameObject& InGameObject )
    {
        // 입력에 따라 주인공의 속도를 조절한다.
        switch ( Controller::GetJoystickDirection() )
        {
            case Direction::LEFT:
            InGameObject.Velocity -= WALK_ACCELERATION;
            break;

            case Direction::RIGHT:
            InGameObject.Velocity += WALK_ACCELERATION;
            break;
        }
    }
};

class PhysicsComponent
{
private:
    FVolume Volume;

public: 
    void Update( GameObject& InGameObject, FWorld& InWorld )
    {
        InGameObject.X += InGameObject.Velocity;
        InWorld.ResolveCollision( Volume, InGameObject.X, InGameObject.Y, InGameObject.Velocity );
    }
};

class GraphicsComponent
{
private:
    FSprite SpriteStand;
    FSprite SpriteWalkLeft;
    FSprite SpriteWalkRight;

public:
     void Update( GameObject& InGameObject, FGraphics& InGraphics )
    {
        // 알맞은 스프라이트를 그린다
        FSprite* sprite = &SpriteStand;
        if ( InGameObject.Velocity < 0 )
            sprite = &SpriteWalkLeft;
        else if ( InGameObject.Velocity > 0 )
            sprite = &SpriteWalkRight;
        
        InGraphics.Draw( *sprite, InGameObject.X, InGameObject.Y );
    }
};

class GameObject
{
private:
    InputComponent Input;
    PhysicsComponent Physics;
    GraphicsComponent Graphics;

public:
    int Velocity;
    int X, Y;

public:
    GameObject() : Velocity( 0 ), X ( 0 ), Y ( 0 ){}
    void Update( FWorld& InWorld, FGraphics& InGraphics )
    {
        Input.Update( *this );
        Physics.Update( *this, InWorld );
        Graphics.Update( *this, InGraphics );
    }
};
  • 각 분야별로 입력, 물리, 그래픽을 컴포넌트화 하여 분리한다.

 

4. 컴포넌트 추상화

class InputComponent
{
public:
    virtual ~InputComponent() {}
    virtual void Update( GameObject& InGameObject ) = 0;
};

class PlayerInputComponent : public InputComponent
{
private:
	static const int WALK_ACCELERATION = 1;

public:
	virtual void Update( GameObject& InGameObject ) override
	{
		// 입력에 따라 주인공의 속도를 조절한다.
		switch ( Controller::GetJoystickDirection() )
		{
			case Direction::LEFT:
			InGameObject.Velocity -= WALK_ACCELERATION;
			break;

			case Direction::RIGHT:
			InGameObject.Velocity += WALK_ACCELERATION;
			break;
		}
	}
};
  • 추상 상위 클래스를 만들어, 사용자에 맞춰 컴포넌트를 정의하여 사용할 수 도 있다.
반응형

+ Recent posts