업데이트 메서드 (Update Method)

컬렉션에 들어 있는 객체별로 한 프레임 단위의 작업을 진행하라고 알려줘서 전체를 시뮬레이션한다.

 

 

동기

while (true) {
    for (double x = 0; x < 100; ++x) skeleton.setX(x);
    for (double x = 100; x > 0; --x) skeleton.setX(x);
}

어떤 몬스터를 좌우로 움직이는 코드를 간단하게 작성하면 위와 같다. 문제는 무한루프이기 때문에 시각적으로 볼 수 없다.

 

Entity skeleton;
bool patrollingLeft = false;
double x = 0;

// 게임 메인 루프
while (true) {
    if (patrollingLeft) {
        --x;
        if (x == 0) patrollingLeft = false;
    }
    else {
        ++x;
        if (x == 100) patrollingLeft = true;
    }
    skeleton.setX(x);
    // 유저 입력 처리, 렌더링 ...
}

게임 루프 구조로 바꾸면 무한루프에 빠지지 않는다.

그런데 다른 몬스터나 오브젝트가 추가되는 경우 유지보수가 점점 어려워진다.

 

해결책은 의외로 간단하다. 모든 객체들이 자신의 동작을 캡슐화 하면 된다.

이를 위해 추상 메서드인 update를 정의하여 추상 계층을 하나 더해서 객체 컬렉션을 관리한다.

게임 루프는 매 프레임마다 객체 컬렉션을 순회하며 update를 호출하면 끝이다.

 

 

패턴

게임 월드는 객체 컬렉션을 관리하고 각 객체는 1프레임 단위의 동작을 하기 위한 업데이트 메서드를 구현한다.

그리고 매 프레임마다 컬렉션에 들어있는 모든 객체를 업데이트한다.

 

 

언제 쓸 것인가?

게임 루프 패턴 다음으로 매우 중요한 패턴이다.

단순한 애니메이션조차 없는 정적인 게임(보드게임 등)이 아닌 이상에야 움직이는 개체가 많은 게임에서는 업데이트 메서드 패턴이 어떻게든 사용된다.

 

◾ 동시에 동작해야 하는 객체나 시스템이 게임에 많다.

◾ 각 객체의 동작은 다른 객체와 거의 독립적이다.

◾ 객체는 시간의 흐름에 따라 시뮬레이션 되어야 한다.

 

위와 같은 경우라면 업데이트 메서드를 사용할 수 있다.

 

 

주의사항

구현 자체가 단순해서 딱히 조심할건 없지만 알아둬야 할 것들은 있다.

 

매 업데이트마다 기존의 상태를 기반으로 행동하기 때문에 현재 상태를 저장해야 하기 때문에 상태 패턴과 사용하면 좋을 수 있다.

 

또한 객체들은 매 프레임마다 업데이트 되지만 동시에 이루어지지 않는다. 내부적으로는 컬렉션에 저장된 순서대로 업데이트가 이루어진다.

순차적으로 업데이트되면 로직 작업이 편하지만 병렬로 업데이트되면 꼬일 가능성이 생긴다.

 

그리고 업데이트를 실행하는 도중에 컬렉션을 수정하는 것은 조심해야한다.

객체가 새로 생성되는 경우라면 목록 뒤에 추가하면 그만이지만 삭제하는 경우는 큰 문제가 발생할 여지가 있다.

 

삭제로 인해 건너뛰어졌다

객체를 삭제해야 하는 경우라면 인덱스를 업데이트 하거나 해당 객체에 죽었다는 표시를 남겨서 순회도중 해당 객체를 만나면 업데이트를 건너뛰고 순회가 끝나면 다시 순회를 돌며 해당 객체들을 제거하는 방법을 사용하는 것이 좋다.

 

 

예제

class Entity {
// 세부내용 생략
public:
    virtual void update() = 0;
}


class World {
public:
    World() : numEntities_(0) {}
    void gameLoop() {
        // ...
        while (true) {
            for (int i = 0; i < numEntities_; ++i)
                entities_[i]->update();
        }
        // ...
    }

private:
    Entity* entities_[MAX_ENTITIES];
    int numEntities_;
};

업데이트가 필요한 클래스들은 Entity를 상속받아서 각자의 행동을 구현하면 된다.

 

그런데 위의 방식은 고정 시간 간격을 쓰는 경우의 구현이다.

앞서 게임 루프에서 언급한것처럼 가변 시간 간격을 사용하는 게임도 있기 때문에 매개변수로 가변 시간 간격을 받아서 처리하면 어렵지 않게 대응할 수 있다.

 

추상 클래스를 이용한 업데이트 메서드 패턴은 상속의 깊이가 한 단계 증가하는 문제가 있지만 상황에 맞게 사용하면 된다.

물론 차후에 다룰 컴포넌트 패턴을 사용하는 것이 더 좋다.

 

 

디자인 결정

중요한건 update 메서드를 어느 클래스에 두느냐이다.

 

◾ 추상 클래스 상속

예제처럼 추상 클래스를 상속하는 것이 가장 간단하지만 요즘은 지양하는 방법이다. 차후 코드 재사용시 단일 상속으로 인한 문제가 발생하면 해결할 방법이 없다.

 

◾ 컴포넌트 클래스

컴포넌트는 알아서 자기 자신을 업데이트 하기 때문에 고민할 필요가 없다.

 

◾ 위임 클래스

상태 패턴처럼 일부 동작을 다른 객체에 위임하고 포워딩만 해주어서 유연성을 얻는다.

상태가 업데이트 되어야 하는 클래스에는 여전히 update가 존재하지만 오버라이딩 한것이 아니라 일반 멤버 함수이다.

 

 

또한 휴면 상태에 들어간 객체의 처리도 생각해 보아야 한다.

여러 이유로 일시적으로 업데이트를 할 필요가 없는 객체들까지 순회하면서 업데이트를 호출하는 것은 자원의 낭비이다.

이를 해결하는 방법으로는 크게 두가지가 있다.

 

단일 컬렉션으로 모두 관리하는 경우에는 플래그 검사를 하더라도 전체 컬렉션을 순회하기 때문에 시간이 낭비되고, 활성 객체만 관리하는 컬렉션을 추가하여 관리하면 시간의 낭비는 없지만 메모리를 추가로 소모해야 할 뿐더러 두 컬렉션의 동기화를 항상 유지해야 한다.

+ Recent posts