跳转到内容

4.6 粒子特效

粒子特效是创建动态视觉效果的强大工具,可以用于制作爆炸、火焰、雪花、星空等效果。本节将介绍如何使用 RmlUi 的动画系统创建简单的粒子效果。


一、粒子特效基础

1.1 什么是粒子特效

粒子特效是由大量小元素(粒子)组成的动态效果,每个粒子都有独立的位置、速度、颜色等属性。

1.2 粒子特效的应用场景

  • 爆炸效果
  • 火焰效果
  • 雪花/雨滴效果
  • 星空效果
  • 魔法效果
  • 点击反馈效果

1.3 实现方式

在 RmlUi 中,粒子特效可以通过以下方式实现:

  • 使用 CSS 动画创建简单粒子
  • 使用数据绑定动态生成粒子
  • 使用 C++ 代码创建复杂粒子系统

二、使用 CSS 动画创建粒子

2.1 基本粒子

xml
<rml>
<head>
    <style>
        .particle {
            position: absolute;
            width: 10px;
            height: 10px;
            background: #ff0000;
            border-radius: 50%;
        }

        @keyframes particle-move {
            0% {
                transform: translate(0, 0) scale(1);
                opacity: 1;
            }
            100% {
                transform: translate(100px, -100px) scale(0);
                opacity: 0;
            }
        }

        .particle.animate {
            animation: particle-move 1s ease-out forwards;
        }
    </style>
</head>
<body>
    <div class="particle"></div>
</body>
</rml>

2.2 爆炸效果

xml
<rml>
<head>
    <style>
        .explosion-container {
            position: relative;
            width: 300px;
            height: 300px;
        }

        .particle {
            position: absolute;
            width: 8px;
            height: 8px;
            background: #ff6600;
            border-radius: 50%;
            opacity: 0;
        }

        @keyframes explode {
            0% {
                transform: translate(0, 0) scale(1);
                opacity: 1;
            }
            100% {
                transform: translate(var(--tx), var(--ty)) scale(0);
                opacity: 0;
            }
        }

        .particle.active {
            animation: explode 1s ease-out forwards;
        }
    </style>
</head>
<body>
    <div class="explosion-container" id="explosion">
        <!-- 粒子将通过 JavaScript 动态生成 -->
    </div>
    <button onclick="triggerExplosion()">爆炸</button>
</body>
</rml>
cpp
// C++ 代码:生成爆炸粒子
void triggerExplosion() {
    auto container = document->GetElementById("explosion");
    
    // 清除旧粒子
    container->SetInnerRML("");
    
    // 生成 20 个粒子
    for (int i = 0; i < 20; i++) {
        // 计算随机方向
        float angle = (i / 20.0f) * 6.28318f;  // 0 到 2π
        float distance = 50.0f + (rand() % 50);  // 50-100px
        
        float tx = cos(angle) * distance;
        float ty = sin(angle) * distance;
        
        // 创建粒子
        String particle_rml = String(256, 
            "<div class='particle' style='--tx: %.0fpx; --ty: %.0fpx;'></div>",
            tx, ty);
        
        auto particle = document->CreateElement("div");
        particle->SetClass("particle", true);
        particle->SetProperty("--tx", String(32, "%.0fpx", tx));
        particle->SetProperty("--ty", String(32, "%.0fpx", ty));
        
        container->AppendChild(std::move(particle));
    }
    
    // 激活动画
    auto particles = container->GetElementsByTagName("div");
    for (size_t i = 0; i < particles.size(); i++) {
        particles[i]->SetClass("active", true);
    }
}

2.3 雪花效果

xml
<rml>
<head>
    <style>
        .snow-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            overflow: hidden;
            pointer-events: none;
        }

        .snowflake {
            position: absolute;
            width: 10px;
            height: 10px;
            background: white;
            border-radius: 50%;
            opacity: 0.8;
        }

        @keyframes fall {
            0% {
                transform: translateY(-10px) translateX(0);
                opacity: 0;
            }
            10% {
                opacity: 0.8;
            }
            90% {
                opacity: 0.8;
            }
            100% {
                transform: translateY(600px) translateX(var(--drift));
                opacity: 0;
            }
        }

        .snowflake.animate {
            animation: fall var(--duration) linear infinite;
            animation-delay: var(--delay);
        }
    </style>
</head>
<body>
    <div class="snow-container" id="snow">
        <!-- 雪花将通过 JavaScript 动态生成 -->
    </div>
</body>
</rml>
cpp
// C++ 代码:生成雪花
void createSnowflakes() {
    auto container = document->GetElementById("snow");
    
    // 生成 50 个雪花
    for (int i = 0; i < 50; i++) {
        // 随机属性
        float x = rand() % 100;  // 0-100%
        float duration = 5.0f + (rand() % 10) / 2.0f;  // 5-10s
        float delay = (rand() % 100) / 10.0f;  // 0-10s
        float drift = (rand() % 200) - 100.0f;  // -100 到 100px
        
        // 创建雪花
        auto snowflake = document->CreateElement("div");
        snowflake->SetClass("snowflake", true);
        snowflake->SetClass("animate", true);
        
        // 设置位置
        snowflake->SetProperty("left", String(32, "%.0f%%", x));
        
        // 设置动画变量
        snowflake->SetProperty("--duration", String(32, "%.1fs", duration));
        snowflake->SetProperty("--delay", String(32, "%.1fs", delay));
        snowflake->SetProperty("--drift", String(32, "%.0fpx", drift));
        
        container->AppendChild(std::move(snowflake));
    }
}

2.4 星空效果

xml
<rml>
<head>
    <style>
        .star-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: #000011;
            overflow: hidden;
        }

        .star {
            position: absolute;
            width: 2px;
            height: 2px;
            background: white;
            border-radius: 50%;
        }

        @keyframes twinkle {
            0%, 100% {
                opacity: 0.3;
                transform: scale(1);
            }
            50% {
                opacity: 1;
                transform: scale(1.5);
            }
        }

        .star.animate {
            animation: twinkle var(--duration) ease-in-out infinite;
            animation-delay: var(--delay);
        }
    </style>
</head>
<body>
    <div class="star-container" id="stars">
        <!-- 星星将通过 JavaScript 动态生成 -->
    </div>
</body>
</rml>
cpp
// C++ 代码:生成星星
void createStars() {
    auto container = document->GetElementById("stars");
    
    // 生成 100 个星星
    for (int i = 0; i < 100; i++) {
        // 随机属性
        float x = rand() % 100;  // 0-100%
        float y = rand() % 100;  // 0-100%
        float duration = 1.0f + (rand() % 30) / 10.0f;  // 1-4s
        float delay = (rand() % 100) / 10.0f;  // 0-10s
        
        // 创建星星
        auto star = document->CreateElement("div");
        star->SetClass("star", true);
        star->SetClass("animate", true);
        
        // 设置位置
        star->SetProperty("left", String(32, "%.0f%%", x));
        star->SetProperty("top", String(32, "%.0f%%", y));
        
        // 设置动画变量
        star->SetProperty("--duration", String(32, "%.1fs", duration));
        star->SetProperty("--delay", String(32, "%.1fs", delay));
        
        container->AppendChild(std::move(star));
    }
}

2.5 点击反馈效果

xml
<rml>
<head>
    <style>
        .ripple-container {
            position: relative;
            overflow: hidden;
        }

        .ripple {
            position: absolute;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.5);
            transform: scale(0);
            pointer-events: none;
        }

        @keyframes ripple-effect {
            to {
                transform: scale(4);
                opacity: 0;
            }
        }

        .ripple.animate {
            animation: ripple-effect 0.6s ease-out forwards;
        }
    </style>
</head>
<body>
    <button class="ripple-container" onclick="createRipple(event)">
        点击我
    </button>
</body>
</rml>
cpp
// C++ 代码:创建涟漪效果
void createRipple(Event& event) {
    auto button = event.GetCurrentElement();
    
    // 获取点击位置
    int x = event.GetParameter<int>("mouse_x", 0);
    int y = event.GetParameter<int>("mouse_y", 0);
    
    // 获取按钮位置
    Vector2f button_position = button->GetAbsoluteOffset();
    Vector2f button_size = button->GetBox().GetSize();
    
    // 计算相对位置
    float relative_x = x - button_position.x;
    float relative_y = y - button_position.y;
    
    // 创建涟漪
    auto ripple = document->CreateElement("div");
    ripple->SetClass("ripple", true);
    ripple->SetClass("animate", true);
    
    // 设置位置(中心在点击点)
    ripple->SetProperty("left", String(32, "%.0fpx", relative_x - 10));
    ripple->SetProperty("top", String(32, "%.0fpx", relative_y - 10));
    ripple->SetProperty("width", "20px");
    ripple->SetProperty("height", "20px");
    
    button->AppendChild(std::move(ripple));
    
    // 动画结束后移除涟漪
    // 注意:需要监听 animationend 事件
}

三、使用数据绑定创建粒子

3.1 基本示例

xml
<rml>
<head>
    <style>
        .particle {
            position: absolute;
            width: 10px;
            height: 10px;
            background: #ff0000;
            border-radius: 50%;
            transition: all 0.3s ease-out;
        }
    </style>
</head>
<body data-model="particles">
    <div data-for="particle in particles">
        <div class="particle"
             data-style-left="particle.x + 'px'"
             data-style-top="particle.y + 'px'"
             data-style-background="particle.color"
             data-style-opacity="particle.opacity">
        </div>
    </div>
</body>
</rml>
cpp
// C++ 代码:粒子系统
struct Particle {
    float x, y;
    float vx, vy;
    String color;
    float opacity;
};

class ParticleSystem {
private:
    std::vector<Particle> particles;
    Element* container;
    
public:
    ParticleSystem(Element* container) : container(container) {
        // 初始化粒子
        for (int i = 0; i < 50; i++) {
            Particle p;
            p.x = rand() % 300;
            p.y = rand() % 300;
            p.vx = (rand() % 100 - 50) / 50.0f;
            p.vy = (rand() % 100 - 50) / 50.0f;
            p.color = "#ff0000";
            p.opacity = 1.0f;
            particles.push_back(p);
        }
        
        // 更新数据绑定
        updateDataModel();
    }
    
    void update() {
        // 更新粒子位置
        for (auto& p : particles) {
            p.x += p.vx;
            p.y += p.vy;
            
            // 边界检查
            if (p.x < 0 || p.x > 300) p.vx *= -1;
            if (p.y < 0 || p.y > 300) p.vy *= -1;
        }
        
        // 更新数据绑定
        updateDataModel();
    }
    
private:
    void updateDataModel() {
        // 创建数据模型
        DataModelHandle model = container->GetDataModel();
        if (model) {
            // 更新粒子数组
            // 注意:需要根据实际的数据绑定 API 实现
        }
    }
};

四、高级粒子系统

4.1 复杂粒子系统架构

cpp
// C++ 代码:粒子系统
class Particle {
public:
    Vector2f position;
    Vector2f velocity;
    Vector2f acceleration;
    float life;
    float max_life;
    Color colour;
    float size;
    
    void update(float dt) {
        velocity += acceleration * dt;
        position += velocity * dt;
        life -= dt;
    }
    
    bool isAlive() const {
        return life > 0;
    }
};

class ParticleEmitter {
private:
    std::vector<Particle> particles;
    Element* container;
    float emission_rate;
    float accumulated_time;
    
public:
    ParticleEmitter(Element* container, float rate) 
        : container(container), emission_rate(rate), accumulated_time(0) {}
    
    void emit(int count) {
        for (int i = 0; i < count; i++) {
            Particle p;
            // 初始化粒子属性
            p.position = Vector2f(150, 150);
            p.velocity = Vector2f(
                (rand() % 200 - 100) / 50.0f,
                (rand() % 200 - 100) / 50.0f
            );
            p.acceleration = Vector2f(0, 0.1f);  // 重力
            p.life = 2.0f;
            p.max_life = 2.0f;
            p.colour = Colour(255, 100, 50, 255);
            p.size = 10.0f;
            
            particles.push_back(p);
        }
    }
    
    void update(float dt) {
        // 更新所有粒子
        for (auto& p : particles) {
            p.update(dt);
        }
        
        // 移除死亡粒子
        particles.erase(
            std::remove_if(particles.begin(), particles.end(),
                [](const Particle& p) { return !p.isAlive(); }),
            particles.end()
        );
        
        // 更新显示
        updateDisplay();
    }
    
private:
    void updateDisplay() {
        // 清除旧粒子
        container->SetInnerRML("");
        
        // 创建新粒子元素
        for (const auto& p : particles) {
            float alpha = p.life / p.max_life;
            String color = String(32, "rgba(%d, %d, %d, %.2f)",
                p.colour.red, p.colour.green, p.colour.blue, alpha);
            
            String rml = String(256,
                "<div class='particle' style='left: %.0fpx; top: %.0fpx; "
                "width: %.0fpx; height: %.0fpx; background: %s; "
                "border-radius: 50%%;'></div>",
                p.position.x, p.position.y, p.size, p.size, color.CString());
            
            container->AppendChild(document->CreateElementFromRML(rml));
        }
    }
};

4.2 火焰效果

cpp
class FireParticleEmitter : public ParticleEmitter {
public:
    FireParticleEmitter(Element* container) 
        : ParticleEmitter(container, 10.0f) {}
    
    void emit(int count) override {
        for (int i = 0; i < count; i++) {
            Particle p;
            p.position = Vector2f(
                150 + (rand() % 40 - 20),
                250
            );
            p.velocity = Vector2f(
                (rand() % 100 - 50) / 100.0f,
                -(rand() % 100 + 50) / 50.0f  // 向上移动
            );
            p.acceleration = Vector2f(0, -0.05f);
            p.life = 1.0f + (rand() % 100) / 200.0f;
            p.max_life = p.life;
            
            // 火焰颜色:从黄色到红色
            int red = 255;
            int green = 150 + (rand() % 105);
            int blue = 50;
            p.colour = Colour(red, green, blue, 255);
            p.size = 15.0f + (rand() % 10);
            
            particles.push_back(p);
        }
    }
};

4.3 爆炸效果

cpp
class ExplosionParticleEmitter : public ParticleEmitter {
public:
    ExplosionParticleEmitter(Element* container) 
        : ParticleEmitter(container, 100.0f) {}
    
    void emit(int count) override {
        for (int i = 0; i < count; i++) {
            Particle p;
            
            // 爆炸中心
            p.position = Vector2f(150, 150);
            
            // 随机方向和速度
            float angle = (i / float(count)) * 6.28318f;
            float speed = 50.0f + (rand() % 100);
            
            p.velocity = Vector2f(
                cos(angle) * speed,
                sin(angle) * speed
            );
            p.acceleration = Vector2f(0, 0.2f);  // 重力
            p.life = 1.5f;
            p.max_life = p.life;
            
            // 爆炸颜色
            int color_index = rand() % 3;
            if (color_index == 0)
                p.colour = Colour(255, 100, 50, 255);  // 橙色
            else if (color_index == 1)
                p.colour = Colour(255, 255, 100, 255);  // 黄色
            else
                p.colour = Colour(255, 50, 50, 255);   // 红色
            
            p.size = 8.0f + (rand() % 8);
            
            particles.push_back(p);
        }
    }
};

五、性能优化

5.1 粒子数量控制

cpp
class ParticleSystem {
private:
    static const int MAX_PARTICLES = 100;  // 最大粒子数
    
public:
    void emit(int count) {
        // 检查粒子数量限制
        int available = MAX_PARTICLES - particles.size();
        int to_emit = std::min(count, available);
        
        for (int i = 0; i < to_emit; i++) {
            // 创建粒子...
        }
    }
};

5.2 使用对象池

cpp
class ParticlePool {
private:
    std::vector<Particle> pool;
    std::vector<bool> active;
    
public:
    ParticlePool(int size) {
        pool.resize(size);
        active.resize(size, false);
    }
    
    Particle* acquire() {
        for (size_t i = 0; i < pool.size(); i++) {
            if (!active[i]) {
                active[i] = true;
                return &pool[i];
            }
        }
        return nullptr;  // 池已满
    }
    
    void release(Particle* particle) {
        size_t index = particle - pool.data();
        if (index < pool.size()) {
            active[index] = false;
        }
    }
};

5.3 批量更新

cpp
void ParticleSystem::update(float dt) {
    // 批量更新所有粒子
    for (auto& p : particles) {
        p.update(dt);
    }
    
    // 一次性移除所有死亡粒子
    auto new_end = std::remove_if(particles.begin(), particles.end(),
        [](const Particle& p) { return !p.isAlive(); });
    particles.erase(new_end, particles.end());
}

5.4 使用 CSS 变量优化

css
/* 使用 CSS 变量减少样式计算 */
.particle {
    --x: 0px;
    --y: 0px;
    --size: 10px;
    --color: #ff0000;
    --opacity: 1;
    
    position: absolute;
    left: var(--x);
    top: var(--y);
    width: var(--size);
    height: var(--size);
    background: var(--color);
    opacity: var(--opacity);
    border-radius: 50%;
}

六、实际应用示例

6.1 成就解锁效果

cpp
void createAchievementUnlockEffect(Element* container) {
    // 创建光环效果
    auto ring = document->CreateElement("div");
    ring->SetClass("achievement-ring", true);
    container->AppendChild(std::move(ring));
    
    // 创建爆炸粒子
    auto emitter = std::make_unique<ExplosionParticleEmitter>(container);
    emitter->emit(50);
    
    // 创建星星粒子
    for (int i = 0; i < 20; i++) {
        auto star = document->CreateElement("div");
        star->SetClass("star", true);
        
        float angle = (i / 20.0f) * 6.28318f;
        float distance = 80.0f;
        float x = 150 + cos(angle) * distance;
        float y = 150 + sin(angle) * distance;
        
        star->SetProperty("left", String(32, "%.0fpx", x));
        star->SetProperty("top", String(32, "%.0fpx", y));
        container->AppendChild(std::move(star));
    }
}

6.2 技能释放效果

cpp
void createSkillEffect(Element* container, const String& skill_type) {
    if (skill_type == "fireball") {
        auto fire = std::make_unique<FireParticleEmitter>(container);
        fire->emit(30);
    }
    else if (skill_type == "ice") {
        // 创建冰晶效果
        for (int i = 0; i < 20; i++) {
            auto crystal = document->CreateElement("div");
            crystal->SetClass("ice-crystal", true);
            
            float x = 100 + (rand() % 100);
            float y = 100 + (rand() % 100);
            crystal->SetProperty("left", String(32, "%.0fpx", x));
            crystal->SetProperty("top", String(32, "%.0fpx", y));
            container->AppendChild(std::move(crystal));
        }
    }
}

七、常见问题

7.1 粒子性能问题

问题:粒子数量多时性能下降。

解决方案

  • 限制粒子数量
  • 使用对象池
  • 批量更新
  • 使用 CSS 变量优化

7.2 粒子重叠问题

问题:粒子之间相互遮挡。

解决方案

css
.particle {
    z-index: var(--z-index);
}

/* 在 C++ 中设置 z-index */
particle->SetProperty("--z-index", String(16, "%d", zIndex));

7.3 粒子内存泄漏

问题:粒子未正确释放导致内存泄漏。

解决方案

cpp
// 确保正确移除粒子元素
void ParticleSystem::updateDisplay() {
    // 清除旧粒子
    while (container->GetFirstChild()) {
        container->RemoveChild(container->GetFirstChild());
    }
    
    // 创建新粒子...
}

八、实战练习

练习 1:创建烟花效果

使用粒子系统创建烟花效果:

  • 爆炸时的火花
  • 火花的重力效果
  • 颜色渐变

练习 2:实现雨滴效果

创建下雨效果:

  • 雨滴从上到下落下
  • 雨滴有随机速度和大小
  • 落地后有溅射效果

练习 3:设计魔法效果

创建魔法释放效果:

  • 粒子沿曲线运动
  • 粒子颜色变化
  • 粒子尾迹效果

练习 4:制作点击特效

为按钮添加点击特效:

  • 点击时产生涟漪
  • 涟漪有颜色变化
  • 支持多次点击

九、总结

粒子特效是创建动态视觉效果的重要工具。通过合理使用 CSS 动画、数据绑定和 C++ 代码,可以创建各种丰富的粒子效果。

关键要点

  • 简单效果使用 CSS 动画
  • 复杂效果使用 C++ 粒子系统
  • 注意性能优化
  • 合理控制粒子数量

十、下一步

恭喜你完成了阶段四的学习!现在可以继续前往 阶段五:高级定制与扩展


📝 检查清单

基于 MIT 许可发布