객체를 매번 할당/해제하지 않고 고정 크기 풀에 들어있는 객체를 재사용하여 메모리 사용 성능을 개선
게임에서 스킬을 사용할 때마다 보이는 이펙트를 위해 파티클이 필요하다.
이때 파티클을 굉장히 빠르게 만들어야 하면서, 파티클을 생성/제거하는 과정에서 메모리 단편화가 생겨서는 안된다.
콘솔, 모바일 게임은 PC에 비해 메모리가 부족하고, 효율 좋은 메모리 관리자를 거의 쓸 수 없어 메모리 단편화에 치명적이다.
객체 풀(=오브젝트 풀) 패턴을 사용하면 메모리 관리자는 처음에 한 번 메모리를 크게 잡아놓고 게임이 실행되는 동안 계속 들고 있을 수 있다.
사용자는 마음껏 객체를 할당, 해제할 수 있다.
준비
사용
주로 게임 개체나 시각적 효과같이 눈으로 볼 수 있는 것이나 사운드 등에 사용된다.
풀이 너무 작으면 크래시가 날 것이고, 풀이 너무 크면 메모리 낭비다.
객체 풀의 모든 개게가 사용 중이어서 재사용할 객체를 반환받지 못할 때를 대비해야 한다.
배열 한 칸의 크기는 가장 큰 객체가 기준이므로, 객체 크기의 편차가 크면 낭비되는 메모리가 많아진다.
따라서 객체 크기 별로 풀을 나누는 게 좋다.
오브젝트 풀은 메모리 고나리자를 통하지 않고 객체를 재사용한다.
때문에 새로운 객체라고 할당받았어도 이전 객체의 상태가 남아있을 수 있다.
따라서 풀에서 새로운 객체를 초기화할 때 객체를 완전히 초기화해야 한다.
오브젝트 풀은 객체가 사용 중이 아니어도 메모리를 해제하지 않는데,
이때 객체가 다른 객체를 참조하고 있다면 GC에서 그 객체를 회수할 수 없다.
사용하지 않는 객체는 다른 객체를 참조하는 부분을 모두 정리해야 한다.
// 한 쪽 방향으로 이동하고 사라질 파티클
class Particle {
public:
Particle() : frameLeft_(0) {} // '사용 안 함'으로 초기화
void init(double x, double y, double xVel, double yVel, int lifetime);
void animate();
bool inUse() const { return frameLeft_ > 0; }
private:
int frameLeft_;
double x_, y_;
double xVel_, yVel_;
};
void Particle::init(double x, double y, double xVel, double yVel, int lifetime) {
x_ = x;
y_ = y;
xVel_ = xVel;
yVel_ = yVel;
frameLeft_ = lifetime;
}
// 매 프레임마다 호출됨 (아마도 업데이트 메서드)
void Particle::animate() {
if (!inUse()) return;
frameLeft_--;
x_ += xVel_;
y_ += yVel_;
}
class ParticlePool {
public:
void create(double x, double y, double xVel, double yVel, int lifetime);
void animate();
private:
// 이런 정적 배열 말고도, 동적 배열의 크기를 지정하거나 템플릿 매개변수로 크기를 조절하는 방법도 있음
static const int POOL_SIZE = 100;
Particle particles_[POOL_SIZE];
};
// 매 프레임마다 Particle::animate 호출
void ParticlePool::animate() {
for (int i = 0; i < POOL_SIZE; ++i) {
particles_[i].animate();
}
}
// 새로운 파티클 생성
void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime) {
for (int i = 0; i < POOL_SIZE; ++i) {
// 사용 가능한 파티클 찾아 초기화
if (!particles_[i].inUse()) {
particles_[i].init(x, y, xVel, yVel, lifetime);
return;
}
}
}
이렇게 오브젝트 풀 패턴을 이용한 파티클 시스템을 완성했다.
그러나 여기에는 파티클을 생성할 때마다 전체 컬렉션을 순회하는 단점이 있다.
사용 가능한 객체의 리스트가 있으면 좋겠지만, 이는 추가적인 메모리를 필요로 한다.
메모리를 희생하지 않고도 해결하기 위해 사용하지 않는 파티클 객체의 일부를 활용해보자.
class Particle {
public:
Particle* getNext() const { return state_.next; }
void setNext(Particle* next) {
state_.next = next;
}
private:
int frameLeft_;
union {
// 사용 중일 때의 상태
struct {
double x, y;
double xVel, yVel;
} live;
// 사용 중이 아닐 때의 상태
Particle* next;
} state_;
};
사용하지 않는 파티클은 frameLeft_
외의 데이터는 의미가 없다. frameLeft_
외의 모든 멤버 변수를 live
구조체 안으로 옮겼다.
next
는 이 파티클 다음에 사용 가능한 파티클 객체 포인터다. 이를 통해 사용 가능한 객체의 연결 리스트, 즉 빈칸 리스트를 만들었다.
class ParticlePool {
// ...
private:
Particle* firstAvailable_; // 연결 리스트의 head
};
ParticlePool::ParticlePool() {
firstAvailable_ = &particles_[0];
for (int i = 0; i < POOL_SIZE - 1; ++i)
particles_[i].setNext(&particles_[i+1]);
particles_[POOL_SIZE-1].setNext(nullptr);
}
void ParticlePool::create(double x, double y, double xVel, double yVel, int lifetime) {
// 사용 가능한 객체가 없는지 확인
assert(firstAvailable_ != nullptr);
Particle* newParticle = firstAvailable_;
firstAvailable_ = newParticle->getNext();
newParticle->init(x, y, xVel, yVel, lifetime);
}
초기화할 때 빈칸 리스트를 만들고, create
로 얻은 파티클은 빈칸 리스트에서 제외한다.
bool Particle::animate() {
if (!inUse()) return false;
frameLeft_--;
x_ += xVel_;
y_ += yVel_;
return frameLeft_ == 0;
}
void ParticlePool::animate() {
for (int i = 0; i < POOL_SIZE; ++i) {
if (particles_[i].animate()) {
particles_[i].setNext(firstAvailable_);
firstAvailable_ = &particles_[i];
}
}
}
파티클이 죽었을 때 다시 빈칸 리스트에 추가해야 하므로 animate
에서 죽으면 true
를 리턴한다.
죽은 파티클은 빈칸 리스트의 head가 된다.
객체가 자신이 풀에 있는 지를 알게 할 것인지를 결정해야 한다.
(아무 객체나 담는 풀 클래스의 경우에는 이렇게 하기 힘들다.)
inUse
메서드로 제공했다. (추가 메모리 할당이 필요없다!)template <class TObject>
clas GenericPool {
private:
static const int POOL_SIZE = 100;
TObject pool_[POOL_SIZE];
bool inUse[POOL_SIZE]; // TObject 외부에서 관리
};
class Particle {
void init(double x, double y);
void init(double x, double y, double angle);
void init(double x, double y, double xVel, double yVel);
};
class ParticlePool {
public:
void create(double x, double y) {
// Particle 클래스로 포워딩
}
void create(double x, double y, double angle) {
// Particle 클래스로 포워딩
}
void create(double x, double y, double xVel, double yVel) {
// Particle 클래스로 포워딩
}
};
class Particle {
void init(double x, double y);
void init(double x, double y, double angle);
void init(double x, double y, double xVel, double yVel);
};
class ParticlePool {
public:
Particle* create() {
// 사용 가능한 파티클에 대한 레퍼런스 리턴
}
private:
Particle pool_[100];
};
// 예제
ParticlePool pool;
pool.create()->init(1, 2);
pool.create()->init(1, 2, 0.3);
pool.create()->init(1, 2, 3.3, 4.4);
craete
가 null을 반환할 수 있으므로 검사가 필요하다.Particle* particle = pool.create();
if (particle != NULL) {
particle->init(1, 2);
}