什么是 ‘Flyweight Pattern’ (享元模式)?在 C++ 游戏开发中管理千万级粒子素材的内存优化

各位同仁,下午好!

今天,我们将深入探讨一个在高性能C++游戏开发中至关重要的设计模式——享元模式(Flyweight Pattern)。特别是,我们将聚焦于如何运用它来优化千万级粒子系统的内存管理,这在现代视觉效果日益丰富的游戏中,是一个实实在在的挑战。

引言:千万级粒子系统的内存梦魇

在当今的游戏引擎中,粒子系统是构建火焰、烟雾、爆炸、魔法效果、雨雪等视觉特效的基石。为了追求极致的视觉冲击力,游戏开发者往往需要同时渲染成千上万,甚至数百万、千万级的粒子。当每个粒子都包含其完整的数据时,内存开销会迅速变得无法承受。

让我们来估算一下。假设一个粒子(Particle)结构体包含以下基本信息:

  • position: glm::vec3 (12 bytes)
  • velocity: glm::vec3 (12 bytes)
  • acceleration: glm::vec3 (12 bytes)
  • color: glm::vec4 (16 bytes)
  • startSize: float (4 bytes)
  • endSize: float (4 bytes)
  • currentSize: float (4 bytes)
  • startLifetime: float (4 bytes)
  • currentLifetime: float (4 bytes)
  • rotation: float (4 bytes)
  • rotationSpeed: float (4 bytes)
  • textureID: unsigned int (4 bytes) (指向纹理资源的ID)
  • animationFrameIndex: unsigned int (4 bytes) (如果粒子有动画)
  • blendMode: unsigned char (1 byte)
  • flags: unsigned char (1 byte)

为了简化计算,我们假设这个结构体在内存中是紧凑排列的,没有额外的对齐填充,总计约 90字节。在实际情况中,由于编译器对齐和指针开销,一个粒子实例可能会更大。我们取 100字节/粒子 作为保守估计。

现在,让我们看看不同数量级的粒子所需的内存:

粒子数量 总内存(字节) 总内存(MB) 总内存(GB)
1,000 100,000 0.1
10,000 1,000,000 1
100,000 10,000,000 10
1,000,000 100,000,000 100 0.1
10,000,000 1,000,000,000 1,000 1
100,000,000 10,000,000,000 10,000 10

可以看到,仅仅1000万个粒子,就需要惊人的1GB内存。如果每个粒子还包含一些更复杂的行为数据、更多的动画帧信息或指向其他资源的智能指针,这个数字还会急剧膨胀。对于一个现代3A游戏,1GB的CPU内存用于粒子数据是极其奢侈的,更别提10GB了。这还没有计算GPU显存的开销。

问题在于,许多粒子,尤其是同一类型的粒子(例如,同一个火焰特效中的所有火星),它们的大部分数据是重复的。例如,它们可能使用相同的纹理、相同的混合模式、相同的生命周期曲线、相同的初始大小范围等。如果每个粒子实例都独立存储这些共享数据,无疑会造成巨大的内存浪费。

这就是享元模式大显身手的地方。

享元模式核心思想

享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享来有效地支持大量细粒度对象。它的核心思想是:将一个对象的状态分解为内在状态(Intrinsic State)外在状态(Extrinsic State)

  • 内在状态(Intrinsic State):是对象固有的、可共享的,不随其上下文改变而改变。它通常存储在享元对象内部,并且可以在多个享元实例之间共享。
  • 外在状态(Extrinsic State):是对象变化的、不可共享的,依赖于其上下文。它通常由客户端存储或计算,并作为参数传递给享元对象的方法。

通过将内在状态抽取出来并共享,享元模式可以极大地减少内存开销,因为相同内在状态的对象可以只创建一份享元实例。

一个简单的类比:图书馆的藏书

想象一个图书馆。

  • 内在状态:书的内容、作者、出版社、ISBN号。这些信息对于同一本书的任何副本都是一样的。
  • 外在状态:这本书当前是否被借出、被谁借走、归还日期。这些信息对于每一本具体的书(即使是同一本书的不同副本)都是独立的。

图书馆不会为每本《三国演义》都单独存储其内容,它只存储一份《三国演义》的内容(内在状态),然后通过借阅卡(外在状态)来管理每本具体的《三国演义》副本的流通情况。

粒子系统的享元设计

回到粒子系统。我们可以将粒子数据划分为:

1. 粒子的内在状态(Intrinsic State)—— ParticleType (享元)

这部分数据定义了一种类型的粒子的通用属性。它不随单个粒子的生命周期而变化,是所有同类型粒子共享的。

  • 渲染相关
    • textureID:使用的纹理ID或纹理句柄。
    • shaderProgramID:使用的着色器程序ID。
    • blendMode:混合模式(例如,加法、阿尔法混合)。
    • renderFlags:渲染标志(例如,是否受光照影响,是否深度测试)。
    • uvRects:纹理动画的UV坐标矩形数组。
  • 行为相关
    • initialSizeRange:初始大小的范围(min, max)。
    • sizeOverLifetimeCurve:生命周期中大小变化的曲线。
    • initialColorRange:初始颜色的范围。
    • colorOverLifetimeCurve:生命周期中颜色变化的曲线。
    • initialSpeedRange:初始速度的范围。
    • gravityModifier:受重力影响的程度。
    • drag:空气阻力。
    • lifetimeRange:生命周期的范围(min, max)。
    • initialRotationRange:初始旋转角度的范围。
    • rotationSpeedRange:旋转速度的范围。
    • emissionShape:粒子发射的形状(例如,点、球体、锥体)。
    • emissionRate:发射速率。

2. 粒子的外在状态(Extrinsic State)—— ParticleInstanceData (客户端数据)

这部分数据定义了每个单独粒子实例的独特属性,它们是动态变化的。

  • position: 当前位置。
  • velocity: 当前速度。
  • acceleration: 当前加速度。
  • currentColor: 当前颜色。
  • currentSize: 当前大小。
  • currentLifetime: 当前剩余生命周期。
  • currentRotation: 当前旋转角度。
  • seed: 随机数种子(可选,用于在生命周期内生成变化)。

3. 享元工厂(Flyweight Factory)—— ParticleTypeFactory

这个工厂负责管理 ParticleType 享元对象。当客户端需要一个特定类型的粒子时,它会首先检查工厂中是否已经存在该类型的享元。如果存在,就返回已有的实例;如果不存在,就创建一个新的享元实例并存储起来,然后返回。

C++ 代码实现

我们将逐步构建这些组件。

1. 定义粒子类型(内在状态的描述)

首先,我们需要一种方式来唯一标识一种粒子类型。这通常通过一个结构体或枚举来完成,其中包含创建该 ParticleType 实例所需的所有可变参数。

// ParticleTypeDefinition.h
#pragma once

#include <string>
#include <vector>
#include <glm/glm.hpp>

// 假设我们有TextureManager和ShaderManager来管理资源
// 只是一个占位符,实际游戏中会更复杂
struct TextureHandle { unsigned int id; };
struct ShaderHandle { unsigned int id; };

// 曲线定义(简化,实际可能是Bezier或Catmull-Rom)
struct FloatCurvePoint { float time; float value; };
struct ColorCurvePoint { float time; glm::vec4 color; };

// 粒子混合模式
enum class ParticleBlendMode : unsigned char {
    Additive,
    Alpha,
    Multiply,
    // ... 其他混合模式
};

// 纹理UV动画帧
struct UVRect {
    glm::vec2 minUV;
    glm::vec2 maxUV;
};

// 用于定义一种粒子类型的配置
struct ParticleTypeDefinition {
    std::string name; // 粒子类型的唯一名称

    // 渲染相关
    std::string texturePath; // 纹理路径,由工厂加载
    std::string shaderPath;  // 着色器路径,由工厂加载
    ParticleBlendMode blendMode = ParticleBlendMode::Alpha;
    std::vector<UVRect> uvFrames; // 纹理动画帧
    float frameRate = 0.0f; // 动画帧率,0表示无动画

    // 行为相关
    glm::vec2 initialSizeRange = {0.1f, 0.2f};
    std::vector<FloatCurvePoint> sizeOverLifetime; // 生命周期大小曲线
    glm::vec4 initialColor = {1.0f, 1.0f, 1.0f, 1.0f}; // 初始颜色,简化为单色
    std::vector<ColorCurvePoint> colorOverLifetime; // 生命周期颜色曲线

    glm::vec2 initialSpeedRange = {1.0f, 3.0f};
    float gravityModifier = 1.0f;
    float drag = 0.0f;
    glm::vec2 lifetimeRange = {1.0f, 2.0f};
    glm::vec2 initialRotationRange = {0.0f, 360.0f}; // 弧度
    glm::vec2 rotationSpeedRange = {0.0f, 180.0f}; // 弧度/秒

    // 比较操作符,用于在map中查找
    bool operator==(const ParticleTypeDefinition& other) const {
        // 实际游戏中,这个比较会非常复杂,可能需要计算哈希值
        // 这里简化,仅比较部分关键字段
        return name == other.name &&
               texturePath == other.texturePath &&
               shaderPath == other.shaderPath &&
               blendMode == other.blendMode; // 仅比较部分,实际需要深度比较所有字段
    }
};

// 为unordered_map提供哈希函数
namespace std {
    template <> struct hash<ParticleTypeDefinition> {
        size_t operator()(const ParticleTypeDefinition& def) const {
            // 一个简单的哈希组合,实际应用中应更健壮
            size_t h1 = hash<string>()(def.name);
            size_t h2 = hash<string>()(def.texturePath);
            size_t h3 = hash<string>()(def.shaderPath);
            return h1 ^ (h2 << 1) ^ (h3 << 2);
        }
    };
}

2. 享元对象:ParticleType (内在状态)

ParticleType 类将持有所有共享的内在状态,并提供方法来根据外在状态进行渲染或计算。

// ParticleType.h
#pragma once

#include "ParticleTypeDefinition.h"
#include <glm/glm.hpp>
#include <memory> // For shared_ptr

// 前向声明,表示一个粒子实例的外在状态
struct ParticleInstanceData;

class ParticleType {
public:
    ParticleType(const ParticleTypeDefinition& def, TextureHandle tex, ShaderHandle shader);

    // 获取粒子类型名称
    const std::string& GetName() const { return m_definition.name; }

    // 获取纹理句柄
    TextureHandle GetTexture() const { return m_texture; }

    // 获取着色器句柄
    ShaderHandle GetShader() const { return m_shader; }

    // 根据生命周期比例获取大小
    float GetSizeOverLifetime(float lifeRatio) const;

    // 根据生命周期比例获取颜色
    glm::vec4 GetColorOverLifetime(float lifeRatio) const;

    // 获取UV动画帧
    const UVRect& GetUVFrame(float lifeRatio) const;

    // 辅助函数:根据曲线获取值
    static float SampleFloatCurve(float time, const std::vector<FloatCurvePoint>& curve);
    static glm::vec4 SampleColorCurve(float time, const std::vector<ColorCurvePoint>& curve);

    // ... 其他getter方法,用于访问定义中的行为属性

private:
    ParticleTypeDefinition m_definition; // 存储一份完整的定义,方便查询和调试
    TextureHandle m_texture;
    ShaderHandle m_shader;

    // 实际游戏中,这里可能还会缓存一些预计算的数据,例如曲线采样点等
};

// ParticleType.cpp
#include "ParticleType.h"
#include <algorithm> // for std::lower_bound

ParticleType::ParticleType(const ParticleTypeDefinition& def, TextureHandle tex, ShaderHandle shader)
    : m_definition(def), m_texture(tex), m_shader(shader) {
    // 可以在这里进行一些预处理,例如对曲线点进行排序
    std::sort(m_definition.sizeOverLifetime.begin(), m_definition.sizeOverLifetime.end(),
              [](const FloatCurvePoint& a, const FloatCurvePoint& b) { return a.time < b.time; });
    std::sort(m_definition.colorOverLifetime.begin(), m_definition.colorOverLifetime.end(),
              [](const ColorCurvePoint& a, const ColorCurvePoint& b) { return a.time < b.time; });
}

float ParticleType::GetSizeOverLifetime(float lifeRatio) const {
    if (m_definition.sizeOverLifetime.empty()) {
        return 1.0f; // 默认不变化
    }
    return SampleFloatCurve(lifeRatio, m_definition.sizeOverLifetime);
}

glm::vec4 ParticleType::GetColorOverLifetime(float lifeRatio) const {
    if (m_definition.colorOverLifetime.empty()) {
        return m_definition.initialColor; // 默认不变化
    }
    return SampleColorCurve(lifeRatio, m_definition.colorOverLifetime);
}

const UVRect& ParticleType::GetUVFrame(float lifeRatio) const {
    if (m_definition.uvFrames.empty()) {
        static UVRect defaultUV = {{0.0f, 0.0f}, {1.0f, 1.0f}};
        return defaultUV; // 返回默认UV
    }
    if (m_definition.frameRate <= 0.0f) {
        return m_definition.uvFrames[0]; // 无动画,返回第一帧
    }
    float totalTime = 1.0f / m_definition.frameRate * m_definition.uvFrames.size();
    float animTime = lifeRatio * totalTime; // 假设动画持续整个粒子生命周期
    int frameIndex = static_cast<int>(animTime * m_definition.frameRate) % m_definition.uvFrames.size();
    return m_definition.uvFrames[frameIndex];
}

float ParticleType::SampleFloatCurve(float time, const std::vector<FloatCurvePoint>& curve) {
    if (curve.empty()) return 1.0f;
    if (time <= curve.front().time) return curve.front().value;
    if (time >= curve.back().time) return curve.back().value;

    auto it = std::lower_bound(curve.begin(), curve.end(), FloatCurvePoint{time, 0.0f},
                               [](const FloatCurvePoint& a, const FloatCurvePoint& b) {
                                   return a.time < b.time;
                               });
    // it points to the first element whose time is not less than 'time'
    // If it's the first element, then 'time' is <= curve.front().time (handled above)
    // So 'it' must be >= curve.begin() + 1
    const FloatCurvePoint& p2 = *it;
    const FloatCurvePoint& p1 = *(it - 1);

    float t = (time - p1.time) / (p2.time - p1.time);
    return p1.value * (1.0f - t) + p2.value * t; // 线性插值
}

glm::vec4 ParticleType::SampleColorCurve(float time, const std::vector<ColorCurvePoint>& curve) {
    if (curve.empty()) return {1.0f, 1.0f, 1.0f, 1.0f};
    if (time <= curve.front().time) return curve.front().color;
    if (time >= curve.back().time) return curve.back().color;

    auto it = std::lower_bound(curve.begin(), curve.end(), ColorCurvePoint{time, {}},
                               [](const ColorCurvePoint& a, const ColorCurvePoint& b) {
                                   return a.time < b.time;
                               });
    const ColorCurvePoint& p2 = *it;
    const ColorCurvePoint& p1 = *(it - 1);

    float t = (time - p1.time) / (p2.time - p1.time);
    return p1.color * (1.0f - t) + p2.color * t; // 线性插值
}

3. 享元工厂:ParticleTypeFactory

ParticleTypeFactory 是享元模式的关键。它负责创建和管理 ParticleType 实例,确保每种 ParticleTypeDefinition 只有一个对应的 ParticleType 实例。

// ParticleTypeFactory.h
#pragma once

#include "ParticleType.h"
#include <unordered_map>
#include <string>
#include <memory> // For shared_ptr

// 模拟资源管理器
class TextureManager {
public:
    TextureHandle LoadTexture(const std::string& path) {
        // 实际加载纹理并返回句柄
        // 这里简化为根据路径生成一个ID
        if (m_textures.find(path) == m_textures.end()) {
            m_textures[path] = {s_nextTextureId++};
        }
        return m_textures[path];
    }
private:
    std::unordered_map<std::string, TextureHandle> m_textures;
    static unsigned int s_nextTextureId;
};

class ShaderManager {
public:
    ShaderHandle LoadShader(const std::string& path) {
        // 实际加载着色器并返回句柄
        if (m_shaders.find(path) == m_shaders.end()) {
            m_shaders[path] = {s_nextShaderId++};
        }
        return m_shaders[path];
    }
private:
    std::unordered_map<std::string, ShaderHandle> m_shaders;
    static unsigned int s_nextShaderId;
};

class ParticleTypeFactory {
public:
    // 获取或创建ParticleType实例
    std::shared_ptr<ParticleType> GetParticleType(const ParticleTypeDefinition& def);

    // 释放所有ParticleType实例(例如,在场景切换时)
    void ClearCache();

private:
    std::unordered_map<ParticleTypeDefinition, std::shared_ptr<ParticleType>> m_particleTypes;
    TextureManager m_textureManager;
    ShaderManager m_shaderManager;
};

// ParticleTypeFactory.cpp
#include "ParticleTypeFactory.h"
#include <iostream>

unsigned int TextureManager::s_nextTextureId = 1;
unsigned int ShaderManager::s_nextShaderId = 1;

std::shared_ptr<ParticleType> ParticleTypeFactory::GetParticleType(const ParticleTypeDefinition& def) {
    auto it = m_particleTypes.find(def);
    if (it != m_particleTypes.end()) {
        std::cout << "Reusing existing ParticleType: " << def.name << std::endl;
        return it->second;
    }

    std::cout << "Creating new ParticleType: " << def.name << std::endl;
    // 加载纹理和着色器
    TextureHandle tex = m_textureManager.LoadTexture(def.texturePath);
    ShaderHandle shader = m_shaderManager.LoadShader(def.shaderPath);

    // 创建新的ParticleType实例
    std::shared_ptr<ParticleType> newType = std::make_shared<ParticleType>(def, tex, shader);
    m_particleTypes[def] = newType; // 存储到缓存
    return newType;
}

void ParticleTypeFactory::ClearCache() {
    m_particleTypes.clear();
    std::cout << "ParticleTypeFactory cache cleared." << std::endl;
    // 实际游戏中,这里也可能需要通知TextureManager和ShaderManager卸载不再使用的资源
}

4. 客户端对象:ParticleInstanceDataParticle

ParticleInstanceData 存储每个粒子实例的独特、可变的外在状态。
Particle 类将组合 ParticleType (享元) 和 ParticleInstanceData (外在状态),作为实际的粒子实例。

// Particle.h
#pragma once

#include "ParticleType.h"
#include <glm/glm.hpp>
#include <random> // 用于粒子随机性

struct ParticleInstanceData {
    glm::vec3 position;
    glm::vec3 velocity;
    glm::vec3 acceleration;
    float currentLifetime; // 当前剩余生命周期
    float totalLifetime;   // 总生命周期
    float initialRotation; // 初始旋转角度
    float rotationSpeed;   // 旋转速度
    float initialSize;     // 初始大小
    unsigned int randomSeed; // 用于在生命周期内生成一致的随机值

    // 假设每个粒子都有一个唯一的ID,方便调试或索引
    unsigned long long id;
    static unsigned long long s_nextParticleId;

    ParticleInstanceData() : id(s_nextParticleId++) {}
};

class Particle {
public:
    Particle(std::shared_ptr<ParticleType> type, const glm::vec3& initialPos, std::mt19937& rng);

    // 更新粒子状态
    void Update(float deltaTime);

    // 渲染粒子(通常由粒子系统批量调用)
    // 注意:渲染方法不直接在Particle类中实现,而是由ParticleSystem负责,
    // ParticleType提供渲染所需的数据,ParticleInstanceData提供实例数据
    // 我们在这里只是演示如何获取渲染所需的数据
    void GetRenderData(glm::vec3& outPos, float& outSize, glm::vec4& outColor, const UVRect*& outUV) const;

    bool IsAlive() const { return m_instanceData.currentLifetime > 0.0f; }

    const glm::vec3& GetPosition() const { return m_instanceData.position; }
    const std::shared_ptr<ParticleType>& GetType() const { return m_particleType; }

private:
    std::shared_ptr<ParticleType> m_particleType; // 享元对象
    ParticleInstanceData m_instanceData;         // 外在状态

    std::mt19937 m_rng; // 每个粒子实例有自己的随机数生成器,基于m_instanceData.randomSeed
};

// Particle.cpp
#include "Particle.h"
#include <iostream>

unsigned long long ParticleInstanceData::s_nextParticleId = 0;

Particle::Particle(std::shared_ptr<ParticleType> type, const glm::vec3& initialPos, std::mt19937& rng)
    : m_particleType(std::move(type)), m_rng(rng) // 拷贝外部的rng,以便使用其分布
{
    // 初始化外在状态
    m_instanceData.position = initialPos;
    m_instanceData.acceleration = glm::vec3(0.0f, -9.81f * m_particleType->m_definition.gravityModifier, 0.0f); // 假设重力
    m_instanceData.randomSeed = m_rng(); // 使用传入的rng生成一个种子

    // 使用粒子类型定义的范围和随机数来初始化粒子实例的属性
    std::uniform_real_distribution<float> lifetimeDist(m_particleType->m_definition.lifetimeRange.x, m_particleType->m_definition.lifetimeRange.y);
    m_instanceData.totalLifetime = lifetimeDist(m_rng);
    m_instanceData.currentLifetime = m_instanceData.totalLifetime;

    std::uniform_real_distribution<float> speedDist(m_particleType->m_definition.initialSpeedRange.x, m_particleType->m_definition.initialSpeedRange.y);
    float speed = speedDist(m_rng);
    // 假设初始速度方向是随机的,这里简化为向上
    std::uniform_real_distribution<float> dirDist(-1.0f, 1.0f);
    glm::vec3 randomDir = glm::normalize(glm::vec3(dirDist(m_rng), std::abs(dirDist(m_rng)) + 0.5f, dirDist(m_rng)));
    m_instanceData.velocity = randomDir * speed;

    std::uniform_real_distribution<float> sizeDist(m_particleType->m_definition.initialSizeRange.x, m_particleType->m_definition.initialSizeRange.y);
    m_instanceData.initialSize = sizeDist(m_rng);

    std::uniform_real_distribution<float> rotDist(glm::radians(m_particleType->m_definition.initialRotationRange.x), glm::radians(m_particleType->m_definition.initialRotationRange.y));
    m_instanceData.initialRotation = rotDist(m_rng);
    std::uniform_real_distribution<float> rotSpeedDist(glm::radians(m_particleType->m_definition.rotationSpeedRange.x), glm::radians(m_particleType->m_definition.rotationSpeedRange.y));
    m_instanceData.rotationSpeed = rotSpeedDist(m_rng);

    // 确保粒子有自己的随机数生成器,以便在生命周期内使用
    m_rng.seed(m_instanceData.randomSeed);
}

void Particle::Update(float deltaTime) {
    if (!IsAlive()) return;

    m_instanceData.currentLifetime -= deltaTime;
    if (m_instanceData.currentLifetime <= 0.0f) {
        m_instanceData.currentLifetime = 0.0f; // 粒子死亡
        return;
    }

    // 更新速度和位置
    glm::vec3 currentAcceleration = m_instanceData.acceleration;
    // 应用阻力
    currentAcceleration -= m_instanceData.velocity * m_particleType->m_definition.drag;

    m_instanceData.velocity += currentAcceleration * deltaTime;
    m_instanceData.position += m_instanceData.velocity * deltaTime;

    // 更新旋转(使用初始旋转和旋转速度)
    m_instanceData.initialRotation += m_instanceData.rotationSpeed * deltaTime;
    // 注意:实际应用中,粒子可能还会根据生命周期曲线调整旋转速度
}

void Particle::GetRenderData(glm::vec3& outPos, float& outSize, glm::vec4& outColor, const UVRect*& outUV) const {
    float lifeRatio = 1.0f - (m_instanceData.currentLifetime / m_instanceData.totalLifetime);

    outPos = m_instanceData.position;
    outSize = m_instanceData.initialSize * m_particleType->GetSizeOverLifetime(lifeRatio);
    outColor = m_particleType->GetColorOverLifetime(lifeRatio);
    outUV = &m_particleType->GetUVFrame(lifeRatio);
}

5. 粒子系统模拟(使用示例)

一个简单的粒子系统,负责创建和管理大量的 Particle 对象。

// ParticleSystem.h
#pragma once

#include "Particle.h"
#include "ParticleTypeFactory.h"
#include <vector>
#include <random>

class ParticleSystem {
public:
    ParticleSystem(ParticleTypeFactory& factory);

    void SpawnParticles(const ParticleTypeDefinition& def, const glm::vec3& position, int count);
    void Update(float deltaTime);
    void Render(); // 实际的渲染逻辑,可能需要批量提交到GPU

    size_t GetActiveParticleCount() const { return m_particles.size(); }

private:
    ParticleTypeFactory& m_factory;
    std::vector<Particle> m_particles;
    std::mt19937 m_globalRNG; // 用于生成粒子初始随机性
};

// ParticleSystem.cpp
#include "ParticleSystem.h"
#include <iostream>
#include <algorithm> // For std::remove_if

ParticleSystem::ParticleSystem(ParticleTypeFactory& factory)
    : m_factory(factory), m_globalRNG(std::random_device{}()) {
    m_particles.reserve(100000); // 预留空间
}

void ParticleSystem::SpawnParticles(const ParticleTypeDefinition& def, const glm::vec3& position, int count) {
    std::shared_ptr<ParticleType> particleType = m_factory.GetParticleType(def);
    for (int i = 0; i < count; ++i) {
        m_particles.emplace_back(particleType, position, m_globalRNG);
    }
    std::cout << "Spawned " << count << " particles of type '" << def.name << "'. Total active: " << m_particles.size() << std::endl;
}

void ParticleSystem::Update(float deltaTime) {
    for (auto& particle : m_particles) {
        particle.Update(deltaTime);
    }

    // 移除死亡粒子
    m_particles.erase(std::remove_if(m_particles.begin(), m_particles.end(),
                                     [](const Particle& p) { return !p.IsAlive(); }),
                      m_particles.end());
}

void ParticleSystem::Render() {
    // 实际渲染会更复杂,例如:
    // 1. 遍历m_particles,根据ParticleType分组
    // 2. 为每个ParticleType绑定其纹理和着色器
    // 3. 收集该类型所有粒子的外在状态(位置、大小、颜色、UV等)
    // 4. 将这些数据批量上传到GPU(VBO/SSBO),然后进行实例化渲染

    // 简化:只是打印一些信息
    // std::cout << "Rendering " << m_particles.size() << " particles." << std::endl;
    // For a real game, you would collect render data here.
    // Example:
    /*
    std::vector<glm::vec3> positions;
    std::vector<float> sizes;
    std::vector<glm::vec4> colors;
    // ... other data for GPU instancing
    for (const auto& p : m_particles) {
        glm::vec3 pos;
        float size;
        glm::vec4 color;
        const UVRect* uvRect;
        p.GetRenderData(pos, size, color, uvRect);
        positions.push_back(pos);
        sizes.push_back(size);
        colors.push_back(color);
        // ... store uvRect data, maybe as indices or directly
    }
    // Then use OpenGL/Vulkan/DX12 to draw instances
    */
}

6. 主函数模拟

// main.cpp
#include "ParticleSystem.h"
#include <iostream>
#include <chrono>
#include <thread>

int main() {
    ParticleTypeFactory factory;
    ParticleSystem particleSystem(factory);

    // 定义几种粒子类型
    ParticleTypeDefinition fireParticleDef;
    fireParticleDef.name = "FireParticle";
    fireParticleDef.texturePath = "textures/fire.png";
    fireParticleDef.shaderPath = "shaders/particle_additive.glsl";
    fireParticleDef.blendMode = ParticleBlendMode::Additive;
    fireParticleDef.lifetimeRange = {0.8f, 1.5f};
    fireParticleDef.initialSizeRange = {0.05f, 0.15f};
    fireParticleDef.initialSpeedRange = {0.5f, 1.5f};
    fireParticleDef.gravityModifier = 0.2f; // 火焰受重力影响小
    fireParticleDef.sizeOverLifetime = { {0.0f, 0.1f}, {0.5f, 1.0f}, {1.0f, 0.0f} }; // 先变大后消失
    fireParticleDef.colorOverLifetime = { {0.0f, {1.0f, 0.8f, 0.2f, 1.0f}}, {0.5f, {1.0f, 0.4f, 0.0f, 0.8f}}, {1.0f, {0.2f, 0.0f, 0.0f, 0.0f}} };

    ParticleTypeDefinition smokeParticleDef;
    smokeParticleDef.name = "SmokeParticle";
    smokeParticleDef.texturePath = "textures/smoke.png";
    smokeParticleDef.shaderPath = "shaders/particle_alpha.glsl";
    smokeParticleDef.blendMode = ParticleBlendMode::Alpha;
    smokeParticleDef.lifetimeRange = {2.0f, 4.0f};
    smokeParticleDef.initialSizeRange = {0.2f, 0.5f};
    smokeParticleDef.initialSpeedRange = {0.1f, 0.3f};
    smokeParticleDef.gravityModifier = -0.1f; // 烟雾向上飘
    smokeParticleDef.sizeOverLifetime = { {0.0f, 0.2f}, {0.8f, 1.5f}, {1.0f, 2.0f} }; // 不断变大
    smokeParticleDef.colorOverLifetime = { {0.0f, {0.5f, 0.5f, 0.5f, 0.5f}}, {0.7f, {0.3f, 0.3f, 0.3f, 0.3f}}, {1.0f, {0.1f, 0.1f, 0.1f, 0.0f}} };

    ParticleTypeDefinition sparkParticleDef;
    sparkParticleDef.name = "SparkParticle";
    sparkParticleDef.texturePath = "textures/spark.png";
    sparkParticleDef.shaderPath = "shaders/particle_additive.glsl";
    sparkParticleDef.blendMode = ParticleBlendMode::Additive;
    sparkParticleDef.lifetimeRange = {0.3f, 0.8f};
    sparkParticleDef.initialSizeRange = {0.01f, 0.03f};
    sparkParticleDef.initialSpeedRange = {2.0f, 5.0f};
    sparkParticleDef.gravityModifier = 1.5f; // 火花受重力影响大
    sparkParticleDef.colorOverLifetime = { {0.0f, {1.0f, 0.5f, 0.0f, 1.0f}}, {0.5f, {1.0f, 0.2f, 0.0f, 0.8f}}, {1.0f, {0.5f, 0.0f, 0.0f, 0.0f}} };

    // 场景模拟
    std::cout << "--- Simulation Start ---" << std::endl;

    // 第一次请求FireParticle,会创建新的享元
    particleSystem.SpawnParticles(fireParticleDef, glm::vec3(0.0f, 0.0f, 0.0f), 100000); // 10万火焰粒子
    particleSystem.SpawnParticles(smokeParticleDef, glm::vec3(0.0f, 0.5f, 0.0f), 50000); // 5万烟雾粒子
    particleSystem.SpawnParticles(sparkParticleDef, glm::vec3(0.0f, 0.1f, 0.0f), 200000); // 20万火花粒子

    // 再次请求FireParticle,会复用已有的享元
    particleSystem.SpawnParticles(fireParticleDef, glm::vec3(1.0f, 0.0f, 0.0f), 50000); // 5万火焰粒子

    // 制造一个非常相似但纹理不同的粒子类型,会创建新的享元
    ParticleTypeDefinition fireParticleDef2 = fireParticleDef;
    fireParticleDef2.name = "FireParticle_Blue";
    fireParticleDef2.texturePath = "textures/blue_fire.png";
    fireParticleDef2.initialColor = {0.2f, 0.8f, 1.0f, 1.0f}; // 改变颜色,但纹理路径不同
    particleSystem.SpawnParticles(fireParticleDef2, glm::vec3(-1.0f, 0.0f, 0.0f), 80000); // 8万蓝色火焰粒子

    long long totalParticles = particleSystem.GetActiveParticleCount();
    std::cout << "nTotal active particles initially: " << totalParticles << std::endl;
    std::cout << "Total unique ParticleType instances in factory: " << factory.m_particleTypes.size() << std::endl;

    // 模拟游戏循环
    float totalTime = 0.0f;
    float deltaTime = 1.0f / 60.0f; // 60 FPS
    for (int i = 0; i < 300; ++i) { // 模拟5秒
        std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(deltaTime * 1000)));
        particleSystem.Update(deltaTime);
        particleSystem.Render();
        totalTime += deltaTime;

        if (i % 60 == 0) { // 每秒打印一次
            std::cout << "Time: " << totalTime << "s, Active particles: " << particleSystem.GetActiveParticleCount() << std::endl;
        }
    }

    std::cout << "--- Simulation End ---" << std::endl;

    // 清理缓存
    factory.ClearCache();

    return 0;
}

运行 main.cpp,你会看到输出中 ParticleTypeFactory 会在第一次请求特定 ParticleTypeDefinition 时创建新的 ParticleType,而在后续请求相同 ParticleTypeDefinition 时复用已有的 ParticleType。这正是享元模式的体现。

享元模式的优势

  1. 极大的内存节省:这是最核心的优势。通过共享内在状态,对于千万级的粒子,我们不再需要存储千万份重复的数据,而是只存储几十、几百份 ParticleType 实例,以及千万份轻量级的 ParticleInstanceData

    • 内存对比
      • 无享元:10,000,000 粒子 * 100 bytes/粒子 = 1 GB
      • 有享元
        • 假设100种 ParticleType,每种 ParticleType 实例约 500-1000 bytes (因为它存储了曲线数据,纹理/着色器句柄等)。取1KB。
        • 100 ParticleType * 1KB/type = 100 KB
        • 10,000,000 ParticleInstanceData * 40 bytes/instance (估算,去除共享数据后的体积) = 400 MB
        • 总计约 400.1 MB。相比1GB,内存节省超过60%!这还是在 ParticleType 结构体比 ParticleInstanceData 稍微重一点的情况下。如果内在状态更重,节省会更明显。
  2. 减少对象创建/销毁开销:享元对象一旦创建就可以被复用,避免了频繁的对象实例化和垃圾回收(如果使用GC语言)或内存分配/释放(C++)的开销。

  3. 提高CPU缓存效率:虽然 Particle 对象分散在内存中,但其引用的 ParticleType 享元对象是集中的。更重要的是,在实际渲染时,通常会将所有 ParticleInstanceData 的数据按类型或属性打包成连续的数组(数据导向设计),然后一次性提交给GPU进行实例化渲染。这样可以最大化CPU缓存的利用率。

享元模式的权衡与考量

  1. 引入复杂性:将对象状态拆分为内在和外在,并引入工厂模式进行管理,无疑增加了设计的复杂性。开发者需要清楚地区分哪些数据是共享的,哪些是独特的。
  2. 查找开销:享元工厂在查找或创建享元时,通常会使用哈希表(std::unordered_map)或树形结构(std::map)。虽然平均情况下查询效率很高,但在大量不同的 ParticleTypeDefinition 或哈希冲突严重的情况下,可能会有性能开销。因此,ParticleTypeDefinitionoperator==hash 函数的实现至关重要。
  3. 线程安全:如果 ParticleTypeFactory 在多线程环境下被访问,需要确保其内部的 std::unordered_map 等数据结构是线程安全的,例如使用互斥锁(std::mutex)。
  4. 资源管理:享元对象本身通常会持有对纹理、着色器等资源的句柄。当某个 ParticleType 不再被任何粒子实例引用时,如果希望卸载其关联的资源,需要一套更复杂的引用计数或显式资源管理机制。std::shared_ptr 能够自动管理 ParticleType 实例的生命周期,但在 ParticleType 内部的 TextureHandleShaderHandle 还需要由 TextureManagerShaderManager 进一步管理。

结合数据导向设计 (Data-Oriented Design, DOD)

享元模式解决了“类型”的内存共享问题,而数据导向设计(DOD)则解决了“实例”的内存访问效率问题。两者结合,可以实现极致的性能优化。

在数据导向设计中,我们不会存储 std::vector<Particle> 这种包含智能指针的结构体数组。相反,我们会将所有粒子的外在状态分解成独立的数组:

// 优化后的粒子系统内部存储
class ParticleSystem {
private:
    // ... 其他成员
    std::vector<glm::vec3> m_positions;
    std::vector<glm::vec3> m_velocities;
    std::vector<float> m_currentLifetimes;
    std::vector<float> m_totalLifetimes;
    std::vector<float> m_initialRotations;
    std::vector<float> m_rotationSpeeds;
    std::vector<float> m_initialSizes;
    std::vector<unsigned int> m_randomSeeds;
    std::vector<std::shared_ptr<ParticleType>> m_particleTypesRefs; // 每个粒子实例对应的享元引用

    // 辅助数据,用于记录每个粒子实例在各个数组中的索引
    // 或者直接使用一个ParticleIndex的struct,存储所有索引
};

这种布局的优势在于:

  1. 更好的CPU缓存局部性:在更新粒子时,CPU可以连续地访问相同类型的数据(例如,所有粒子的位置),而不是跳跃式地访问整个 Particle 结构体,这大大减少了缓存未命中的情况。
  2. 更适合SIMD指令:连续的同类型数据数组非常适合利用现代CPU的SIMD(Single Instruction, Multiple Data)指令集进行批量并行处理,进一步加速粒子更新。
  3. GPU实例化渲染的自然匹配:GPU实例化渲染也需要将相同类型的数据(例如,所有实例的位置、颜色、大小)打包成数组上传到GPU。DOD的内存布局直接支持这种模式。

当结合DOD时,Particle 类作为一个完整的对象概念可能会消失,取而代之的是一个索引,它指向各个属性数组中对应的数据。ParticleType 享元则通过索引来获取其对应的 ParticleType 引用。

总结

享元模式是处理大量相似对象内存问题的强大工具。通过将粒子的内在(共享)状态与外在(独特)状态分离,并配合一个高效的享元工厂进行管理,我们能够将千万级粒子系统的内存占用从数十GB级别优化到数百MB级别,甚至更低。在C++游戏开发中,尤其是在粒子系统、地形瓦片、UI元素等场景下,它都是一个值得深入学习和实践的优化策略。

同时,我们也要认识到,引入享元模式会增加代码的复杂性。在设计时,需要仔细权衡其带来的性能收益与开发维护成本。当问题规模达到一定程度,且对象的重复性高时,享元模式的价值才能充分体现。结合数据导向设计,可以进一步提升性能,实现内存与CPU效率的双重优化。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注