Skip to content

Latest commit

 

History

History
2351 lines (1904 loc) · 110 KB

File metadata and controls

2351 lines (1904 loc) · 110 KB

九、改进的粒子系统

我们在上一章开发的粒子系统是一个很好的开始,但是你可以用它创造的效果相当平淡。我们的粒子不会旋转或缩放,也不会被动画化,并且随着时间的推移,它们的外观相对一致。

For this chapter, you will need to include several images in your build to make this project work. Make sure that you include the /Chapter09/sprites/ folder from this project's GitHub repository. If you would like to build the particle system tool from GitHub, the source for the tool is located in the /Chapter09/advanced-particle-tool/ folder. If you haven't downloaded the GitHub project yet, you can get it online here: https://github.com/PacktPublishing/Hands-On-Game-Develop.

如果我们想从我们的粒子系统中得到最大的好处,我们需要给它增加更多的特性。在本章中,我们将添加以下附加功能:

  • 粒子在其寿命期间的尺度
  • 粒子旋转
  • 动画粒子
  • 颜色随时间变化
  • 支持粒子爆发
  • 支持环形和非环形发射器

修改我们的 HTML 外壳文件

我们需要做的第一件事是向 HTML shell 文件中添加一些新的输入。我们将把basic_particle_shell.html文件复制到一个新的 shell 文件中,我们称之为advanced_particle_shell.html。我们将在原始容器和canvas元素之间的外壳文件的 HTML 部分添加第二个容器类div元素和许多新的输入。以下是新容器元素的外观:

<div class="container">
<div class="empty_box">&nbsp;</div><br/>
<span class="label">min start scale:</span>
<input type="number" id="min_starting_scale" max="9.9" min="0.1" step="0.1" value="1.0" class="em_input"><br/>
<span class="label">max start scale:</span>
<input type="number" id="max_starting_scale" max="10.0" min="0.2" step="0.1" value="2.0" class="em_input"><br/>
<span class="label">min end scale:</span>
<input type="number" id="min_end_scale" max="9.9" min="0.1" step="0.1" value="1.0" class="em_input">
<br/>
<span class="label">max end scale:</span>
<input type="number" id="max_end_scale" max="10.0" min="0.2" step="0.1" value="2.0" class="em_input">
<br/>
<span class="label">start color:</span>
<input type="color" id="start_color" value="#ffffff" class="color_input"><br/>
<span class="label">end color:</span>
<input type="color" id="end_color" value="#ffffff" class="color_input"><br/>
<span class="label">burst time pct:</span>
<input type="number" id="burst_time" max="1.0" min="0.0" step="0.05" value="0.0" class="em_input">
<br/>
<span class="label">burst particles:</span>
<input type="number" id="burst_particles" max="100" min="0" step="1" value="0" class="em_input">
<br/>
<label class="ccontainer"><span class="label">loop:</span>
    <input type="checkbox" id="loop" checked="checked">
    <span class="checkmark"></span>
</label>
<br/>
<label class="ccontainer"><span class="label">align rotation:</span>
    <input type="checkbox" id="align_rotation" checked="checked">
    <span class="checkmark"></span>
</label>
<br/>
<span class="label">emit time ms:</span>
<input type="number" id="emit_time" max="10000" min="100" step="100" value="1000" class="em_input">
<br/>
<span class="label">animation frames:</span>
<input type="number" id="animation_frames" max="64" min="1" step="1" value="1" class="em_input">
<br/>
<div class="input_box">
<button id="update_btn" class="em_button" onclick="UpdateClick()">Update Emitter</button>
</div>
</div>

缩放值

缩放精灵意味着将精灵的大小修改为其原始大小的倍数。例如,如果我们按缩放值2.0缩放 16 x 16 的子画面,子画面将作为 32 x 32 的图像渲染到画布上。这个新容器从四个输入元素及其标签开始,它们告诉粒子系统如何在粒子的生命周期内缩放粒子。min_starting_scalemax_starting_scale元素是粒子的起始范围刻度。如果您希望粒子总是以1.0的比例开始(1 比 1 的比例与.png图像大小),您应该将1.0放在这两个字段中。实际的起始刻度值将是随机选择的值,介于您在这些字段中输入的两个值之间。我们没有在这个界面中添加任何检查来验证max是否大于min,所以请确保maxmin值相同或更大,否则会损坏发射器。接下来的两个input元素是min_end_scalemax_end_scale。与起始刻度值一样,实际的结束刻度将是一个随机选择的值,介于我们在这些字段中输入的两个值之间。在粒子寿命的任何给定点,它都有一个标度,该标度是在分配给该粒子寿命开始的标度值和结束时的标度值之间的插值。所以,如果我以1.0的刻度值开始,以3.0的刻度值结束,当粒子寿命过半时,粒子的刻度值就是2.0

以下是这些元素在 HTML 文件中的样子:

<span class="label">min start scale:</span>
<input type="number" id="min_starting_scale" max="9.9" min="0.1" step="0.1" value="1.0" class="em_input"><br/>
<span class="label">max start scale:</span>
<input type="number" id="max_starting_scale" max="10.0" min="0.2" step="0.1" value="2.0" class="em_input"><br/>
<span class="label">min end scale:</span>
<input type="number" id="min_end_scale" max="9.9" min="0.1" step="0.1" value="1.0" class="em_input">
<br/>
<span class="label">max end scale:</span>
<input type="number" id="max_end_scale" max="10.0" min="0.2" step="0.1" value="2.0" class="em_input">
<br/>

混色值

SDL 有一个名为SDL_SetTextureColorMod的功能,能够修改纹理的红色、绿色和蓝色通道。此功能只能减少颜色通道值,因此在灰度图像上使用这些值效果最佳。HTML 中接下来的两个输入是start_colorend_color。这些值将用于修改粒子在其寿命期间的颜色通道。每个颜色通道(红色、绿色和蓝色)都在粒子的生命周期内进行插值。

以下是这些元素在 HTML 文件中的样子:

<span class="label">start color:</span>
<input type="color" id="start_color" value="#ffffff" class="color_input"><br/>
<span class="label">end color:</span>
<input type="color" id="end_color" value="#ffffff" class="color_input"><br/>

粒子爆发

到目前为止,我们研究过的粒子系统已经发出了一致的粒子流。我们可能希望在我们的粒子系统的生命周期内有一个时间点,在这个时间点上,一束粒子同时被发射出去。接下来的两个输入元素是burst_timeburst_particlesburst_time元素允许从0.01.0的值。这个数字代表粒子发射器寿命中爆发发生的部分。0.0的值意味着爆发将发生在发射器生命周期的最开始,1.0将发生在最末端,0.5将发生在中间。在burst_time元素之后是burst_particles元素。该元素包含爆发中发射的粒子数。在调整它使其成为一个大数字之前,请确保您将max_particles输入元素设置为一个可以容纳突发的值。例如,如果您有一个每秒发射20粒子的粒子发射器,并且您的最大粒子数也是20粒子,则添加任何大小的爆发都不会引起注意,因为粒子池中没有足够的非活动粒子供爆发使用。

以下是这些元素在 HTML 文件中的样子:

<span class="label">burst time pct:</span>
<input type="number" id="burst_time" max="1.0" min="0.0" step="0.05" value="0.0" class="em_input">
<br/>
<span class="label">burst particles:</span>
<input type="number" id="burst_particles" max="100" min="0" step="1" value="0" class="em_input">
<br/>

循环发射器

一些发射器执行一段固定的时间,然后在该时间到期时停止。这种发射器的一个例子是爆炸。一旦爆炸效果完成,我们希望它结束。一种不同类型的发射器可能会循环,它将继续执行,直到其他代码停止发射器。这种发射器的一个例子是我们宇宙飞船的发动机废气。只要我们的宇宙飞船在加速,我们就希望看到一串粒子从它的后面发射出来。HTML 中的下一个元素是循环复选框元素。如果点击,发射器将继续发射,甚至在其寿命结束后。如果有一个脉冲与此发射器相关联,则每次发射器通过其回路的该部分时,该脉冲都会出现。

以下是输入元素在 HTML 中的样子:

<label class="ccontainer"><span class="label">loop:</span>
<input type="checkbox" id="loop" checked="checked">
<span class="checkmark"></span>
</label>
<br/>

对齐粒子旋转

旋转可以提升很多粒子效果。我们被迫挑选我们希望在项目中用于粒子系统的值,因为坦率地说,我可以写一本关于粒子系统的书。我们将有一个标志,允许用户选择粒子系统是否将其旋转与发射速度矢量对齐,而不是像前面对粒子尺度所做的那样有旋转值范围。我发现这是一个令人愉快的效果。用户将通过id="align_rotation"复选框做出该决定。

以下是 HTML 代码的样子:

<label class="ccontainer"><span class="label">align rotation:</span>
 <input type="checkbox" id="align_rotation" checked="checked">
 <span class="checkmark"></span>
 </label>
 <br/>

发射时间

发射时间是我们的粒子发射器停止运行之前运行的时间(以毫秒为单位),如果用户勾选了循环复选框,则为循环。如果粒子系统循环,该值仅在具有爆发的粒子系统中可见。这将导致粒子系统每次通过环路时都会发生爆发。

HTML 代码如下:

<span class="label">emit time ms:</span>
<input type="number" id="emit_time" max="10000" min="100" step="100" value="1000" class="em_input"><br/>

动画帧

如果我们想创建一个多帧动画的粒子,我们可以在这里添加帧数。该功能采用水平条状精灵表,将加载的图像文件均匀地分布在 x 轴上。当该值为1时,因为只有一帧,所以没有动画。动画的帧时间将平均分配给单个粒子的生存时间。换句话说,如果你有一个十帧动画,粒子寿命是 1000 毫秒,动画的每一帧将显示 100 毫秒(1000/10)。

以下是 HTML 元素:

<span class="label">animation frames:</span>
<input type="number" id="animation_frames" max="64" min="1" step="1" value="1" class="em_input"><br/>

现在我们已经定义了我们的 HTML,让我们来看看代码的 JavaScript 部分。

修改 JavaScript

我们正在创建的工具在游戏之外运行,我们已经在几个章节中工作了。正因为如此,我们正在开发一个新的 HTML shell 文件,我们将编写大量的 JavaScript 来将我们的用户界面与我们稍后将放入游戏中的 WebAssembly 类集成在一起。让我们花点时间浏览一下我们需要添加到新的 HTML shell 文件中的所有 JavaScript 函数。

JavaScript 更新点击功能

修改完 HTML 之后,接下来我们需要做的就是修改UpdateClick() JavaScript 函数,让它从 HTML 元素中抓取新的值,并将这些值传递给update_emitterModule.ccall函数调用。

以下是全新版本的UpdateClick功能的全部内容:

function UpdateClick() {
    if( ready == false || image_added == false ) {
        return;
    }
    var max_particles = Number(document.getElementById
                        ("max_particles").value);
    var min_angle = Number(document.getElementById
                    ("min_angle").value) / 180 * Math.PI;
    var max_angle = Number(document.getElementById
                    ("max_angle").value) / 180 * Math.PI
    var particle_lifetime = Number(document.getElementById
                            ("lifetime").value);
    var acceleration = Number(document.getElementById
                       ("acceleration").value);
    var alpha_fade = Boolean(document.getElementById
                     ("alpha_fade").checked);
    var emission_rate = Number(document.getElementById
                        ("emission_rate").value);
    var x_pos = Number(document.getElementById
                ("x_pos").value);
    var y_pos = Number(document.getElementById
                ("y_pos").value);
    var radius = Number(document.getElementById
                 ("radius").value);
    var min_starting_velocity = Number(document.getElementById
                                ("min_starting_vel").value);
    var max_starting_velocity = Number(document.getElementById
                                ("max_starting_vel").value);

    /* NEW INPUT PARAMETERS */
    var min_start_scale = Number(document.getElementById
                          ("min_starting_scale").value);
    var max_start_scale = Number(document.getElementById
                          ("max_starting_scale").value);
    var min_end_scale = Number(document.getElementById
                        ("min_end_scale").value);
    var max_end_scale = Number(document.getElementById
                        ("max_end_scale").value);
    var start_color_str = document.getElementById
                          ("start_color").value.substr(1, 7);
    var start_color = parseInt(start_color_str, 16);
    var end_color_str = document.getElementById
                        ("end_color").value.substr(1, 7);
    var end_color = parseInt(end_color_str, 16);
    var burst_time = Number(document.getElementById
                     ("burst_time").value);
    var burst_particles = Number(document.getElementById
                          ("burst_particles").value);
    var loop = Boolean(document.getElementById
               ("loop").checked);
    var align_rotation = Boolean(document.getElementById
                         ("align_rotation").checked);
    var emit_time = Number(document.getElementById
                    ("emit_time").value);
    var animation_frames = Number(document.getElementById
                           ("animation_frames").value);

    Module.ccall('update_emitter', 'undefined', ["number", "number", 
    "number", "number", "number", "bool", "number", "number",
    "number", "number", "number", "number",
    /* new parameters */
    "number", "number", "number", "number", "number", "number", 
    "number", "number", "bool", "bool", "number"],
    [max_particles, min_angle, max_angle, particle_lifetime, 
    acceleration, alpha_fade, min_starting_velocity, 
    max_starting_velocity, emission_rate, x_pos, y_pos, radius,
    /* new parameters */
    min_start_scale, max_start_scale, min_end_scale, max_end_scale,
    start_color, end_color, burst_time, burst_particles,    
    loop, align_rotation, emit_time, animation_frames]);
    }

如您所见,我们在这个 JavaScript 函数中添加了新的局部变量,它将存储我们从新的 HTML 元素中获取的值。检索缩放值并强制将其转换为数字以传递到update_emitter现在应该很熟悉了。这是代码:

var min_start_scale = Number(document.getElementById
                      ("min_starting_scale").value);
var max_start_scale = Number(document.getElementById
                      ("max_starting_scale").value);
var min_end_scale = Number(document.getElementById
                    ("min_end_scale").value);
var max_end_scale = Number(document.getElementById
                    ("max_end_scale").value);

强制颜色值

在 JavaScript 中,变量强制是将一种变量类型转换成另一种变量类型的过程。因为 JavaScript 是弱类型语言,强制与类型转换有点不同,后者类似于 C 和 C++ 等强类型语言中的变量强制。

将我们的颜色值强制转换为Integer值的过程是一个两步走的过程。这些元素中的值是以*#*字符开始的字符串,后跟一个六位十六进制数。我们需要做的第一件事是删除那个开始的#字符,因为它会阻止我们将那个字符串解析成一个整数。我们通过一个简单的substr来获取元素内部值的子字符串(字符串的一部分)。

以下是start_color的情况:

var start_color_str = document.getElementById
                      ("start_color").value.substr(1, 7);

我们知道字符串总是七个字符长,但我们只想要最后六个字符。我们现在有了起始颜色的十六进制表示,但它仍然是一个字符串变量。现在,我们需要将此强制转换为一个Integer值,并且我们必须告诉parseInt函数使用基数 16(十六进制),因此我们将把值16作为第二个参数传递到parseInt中:

var start_color = parseInt(start_color_str, 16);

现在我们已经将start_color强制转换为一个整数,我们将对end_color进行同样的操作:

var end_color_str = document.getElementById
                    ("end_color").value.substr(1, 7);
var end_color = parseInt(end_color_str, 16);

附加可变胁迫

start_colorend_color胁迫之后,剩下的我们必须执行的胁迫应该会觉得熟悉。我们将burst_timeburst_particlesemit_timeanimation_frames中的值强制转换为Number变量。我们将loopalign_rotation的校验值强制转换为布尔变量。

以下是强制代码的剩余部分:

var burst_time = Number(document.getElementById
                 ("burst_time").value);
var burst_particles = Number(document.getElementById
                      ("burst_particles").value);
var loop = Boolean(document.getElementById
           ("loop").checked);
var align_rotation = Boolean(document.getElementById
                     ("align_rotation").checked);
var emit_time = Number(document.getElementById
                ("emit_time").value);
var animation_frames = Number(document.getElementById
                       ("animation_frames").value);

最后,我们需要将变量类型和新变量添加到我们的 WebAssembly 模块中对update_emitterModule.ccall调用中:

Module.ccall('update_emitter', 'undefined', ["number", "number",                                       "number", "number", "number", "bool",
                                  "number", "number", "number",                                           "number", "number","number",
                                            /* new parameters */
                                             "number", "number",
                                             "number", "number",
                                             "number", "number",
                                             "number", "number",
                                             "bool", "bool", "number"],
                                            [max_particles, min_angle, 
                                             max_angle,
                                             particle_lifetime,         
                                             acceleration, alpha_fade,
                                             min_starting_velocity, 
                                             max_starting_velocity,
                                             emission_rate, x_pos, 
                                             y_pos, radius,
                                            /* new parameters */
                                             min_start_scale,   
                                             max_start_scale,
                                             min_end_scale, 
                                             max_end_scale,
                                             start_color, end_color,
                                             burst_time, 
                                             burst_particles,
                                             loop, align_rotation, 
                                             emit_time,
                                             animation_frames
                                         ]);

修改句柄文件功能

我们需要对 HTML shell 文件进行的最后一项更改是对handleFiles函数的修改。这些修改有效地反映了UpdateClick功能的变化。当您遍历代码时,您将看到相同的强制在handleFiles内部复制,并且Module.ccalladd_emitter将使用相同的新参数类型和参数进行更新。以下是最新版本handleFiles功能的代码:

function handleFiles(files) {
    var file_count = 0;
    for (var i = 0; i < files.length; i++) {
        if (files[i].type.match(/image.png/)) {
            var file = files[i]; 
            var file_name = file.name;
            var fr = new FileReader();
            fr.onload = function (file) {
                var data = new Uint8Array(fr.result);
                Module.FS_createDataFile('/', file_name, data, true, true, 
                true);
                var max_particles = Number(document.getElementById
                                    ("max_particles").value);
                var min_angle = Number(document.getElementById
                                ("min_angle").value) / 180 * Math.PI;
                var max_angle = Number(document.getElementById
                                ("max_angle").value) / 180 * Math.PI
                var particle_lifetime = Number(document.getElementById
                                        ("lifetime").value);
                var acceleration = Number(document.getElementById
                                   ("acceleration").value);
                var alpha_fade = Boolean(document.getElementById
                                 ("alpha_fade").checked);
                var emission_rate = Number(document.getElementById
                                    ("emission_rate").value);
                var x_pos = Number(document.getElementById
                                  ("x_pos").value);
                var y_pos = Number(document.getElementById
                                  ("y_pos").value);
                var radius = Number(document.getElementById
                                   ("radius").value);
                var min_starting_velocity = Number(document.getElementById
                                            ("min_starting_vel").value);
                var max_starting_velocity = Number(document.getElementById
                                            ("max_starting_vel").value);

                /* NEW INPUT PARAMETERS */
                var min_start_scale = Number(document.getElementById
                                      ("min_starting_scale").value);
                var max_start_scale = Number(document.getElementById
                                      ("max_starting_scale").value);
                var min_end_scale = Number(document.getElementById
                                    ("min_end_scale").value);
                var max_end_scale = Number(document.getElementById
                                    ("max_end_scale").value);
                var start_color_str = document.getElementById
                                     ("start_color").value.substr(1, 7);
                var start_color = parseInt(start_color_str, 16);
                var end_color_str = document.getElementById
                                    ("end_color").value.substr(1, 7);
                var end_color = parseInt(end_color_str, 16);
                var burst_time = Number(document.getElementById
                                 ("burst_time").value);
                var burst_particles = Number(document.getElementById
                                      ("burst_particles").value);
                var loop = Boolean(document.getElementById
                           ("loop").checked);
                var align_rotation = Boolean(document.getElementById 
                                     ("align_rotation").checked);
                var emit_time = Number(document.getElementById
                                ("emit_time").value);
                var animation_frames = Number(document.getElementById
                                       ("animation_frames").value);

                Module.ccall('add_emitter', 'undefined', 
                ["string","number", "number", "number", 
                "number","number","bool","number","number",
                "number", "number", "number","number", 
                /* new parameters */ 
                "number", "number", "number",
                "number", "number", "number", "number", 
                "number","bool", "bool", "number"],
                    file_name,max_particles,min_angle,max_angle,
                    particle_lifetime,acceleration,alpha_fade,
                    min_starting_velocity,max_starting_velocity,
                    emission_rate, x_pos,y_pos,radius,
                    /* new parameters */ 
                    min_start_scale,max_start_scale,min_end_scale, 
                    max_end_scale,start_color,end_color,
                    burst_time,burst_particles,loop,
                    align_rotation,emit_time,animation_frames ]);
                image_added = true;
            };
            fr.readAsArrayBuffer(files[i]); }}}

现在我们已经有了 JavaScript 代码,我们可以开始对 WebAssembly 模块进行更改了。

修改粒子类

现在我们已经将更改添加到了我们的 HTML shell 文件中,我们需要对我们的 WebAssembly 模块进行一些更改,以支持这些新参数。我们将从底层开始,从Particle班开始。这个类不仅对我们正在构建的设计粒子系统的工具有用,而且它是少数几个类之一,一旦我们完成了它,我们将能够进入我们的游戏,允许我们添加一些漂亮的效果。

以下是game.hpp文件中的粒子类定义:

class Particle {
    public:
        bool m_active;
        bool m_alpha_fade;
        bool m_color_mod;
        bool m_align_rotation;
        float m_rotation;

        Uint8 m_start_red;
        Uint8 m_start_green;
        Uint8 m_start_blue;

        Uint8 m_end_red;
        Uint8 m_end_green;
        Uint8 m_end_blue;

        Uint8 m_current_red;
        Uint8 m_current_green;
        Uint8 m_current_blue;

        SDL_Texture *m_sprite_texture;
        int m_ttl;

        Uint32 m_life_time;
        Uint32 m_animation_frames;
        Uint32 m_current_frame;
        Uint32 m_next_frame_ms;

        float m_acceleration;
        float m_alpha;
        float m_width;
        float m_height;
        float m_start_scale;
        float m_end_scale;
        float m_current_scale;

        Point m_position;
        Point m_velocity;

        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };
        SDL_Rect m_src = {.x = 0, .y = 0, .w = 0, .h = 0 };

        Particle( SDL_Texture *sprite, Uint32 life_time, float 
        acceleration,
                    bool alpha_fade, int width, int height, bool 
                    align_rotation,
                    Uint32 start_color,
                    Uint32 end_color,
                    Uint32 animation_frames );
        void Update( Uint32 life_time, float acceleration,
                     bool alpha_fade, bool align_rotation,
                     Uint32 start_color, Uint32 end_color,
                     Uint32 animation_frames );

        void Spawn( float x, float y, float velocity_x, float 
                velocity_y,
                    float start_scale, float end_scale, float rotation );

        void Move();
        void Render();
};

新属性

我们将遍历添加到Particle类定义中的新属性,并简要讨论每个新属性的作用。我们添加的第一个属性是bool m_color_mod。在我们的 HTML 中,我们没有这个值的复选框,所以你可能想知道为什么这里有一个。原因是性能。如果用户不想修改颜色,打电话到SDL_SetTextureColorMod是浪费。如果我们有两个白色值传递到Particle对象中,则不需要插值或调用来修改该值。我们可以每次检查开始和结束颜色,看看它们的值是否是0xffffff,但我觉得添加这个标志会使检查更加清晰。

对齐旋转属性

接下来的m_align_rotation标志就是我们从复选框传入的标志。如果该值为true,粒子将自身旋转以指向其移动的方向。m_rotation浮点变量紧随其后。保存粒子角度的属性变量将根据粒子移动的方向进行旋转。下面是这些值在我们的代码中的样子:

bool m_align_rotation;
float m_rotation;

颜色属性

我前面提到的颜色 mod 标志使下一组值的检查变得更加容易。我们的十六进制颜色值表示 HTML 中的红色、绿色和蓝色值,需要作为一个整数传入,这样它就可以分解成三个 8 位通道。下面是这些 8 位颜色变量在代码中的样子:

Uint8 m_start_red;
Uint8 m_start_green;
Uint8 m_start_blue;

Uint8 m_end_red;
Uint8 m_end_green;
Uint8 m_end_blue;

Uint8 m_current_red;
Uint8 m_current_green;
Uint8 m_current_blue;

你会注意到这些都是用Uint8声明的 8 位无符号整数变量。当 SDL 执行颜色修改时,它不会将 RGB 值作为单个变量;相反,它将值分解为三个 8 位变量,代表每个单独的通道。m_start_(color)变量和m_end_(color)变量将基于粒子寿命进行插值,以获得m_current_(color)变量,当我们进行颜色修改时,该变量将作为通道传递到 SDL。因为我们将这些值作为一个单一的颜色变量从 JavaScript 传入,Particle构造函数和Update函数将需要执行位操作来设置这些单独的通道变量。

动画属性

下一组新属性都与我们Particle中的新帧动画功能相关。下面是代码中的这些属性:

Uint32 m_animation_frames;
Uint32 m_current_frame;
Uint32 m_next_frame_ms;

第一个属性m_animation_frames是从 JavaScript 间接传递的值。它告诉Particle类,当它将纹理渲染到画布上时,精灵纹理中有多少帧。第二个属性m_current_frame,被Particle类用来跟踪当前应该渲染哪个帧。最后一个属性变量m_next_frame_ms,告诉粒子在必须增加当前帧以显示序列中的下一帧之前还剩下多少毫秒。

大小和比例属性

下一批属性与我们粒子的大小和尺度有关。在这个代码的前一个版本中,我们处理了m_dest矩形的宽度和高度。这不再实用,因为这个矩形的宽度和高度(wh)属性需要修改,以适应我们当前的比例。下面是代码中出现的新变量:

float m_width;
float m_height;

float m_start_scale;
float m_end_scale;
float m_current_scale;

现在需要m_widthm_height属性来跟踪粒子的原始宽度和高度,它们还没有被比例调整。

m_start_scalem_end_scale属性是在我们在 JavaScript 中定义的maxmin值之间随机选择的值。

m_current_scale属性是渲染粒子时计算m_dest.wm_dest.h值时使用的当前比例。当前比例将是介于m_start_scalem_end_scale属性之间的插值。

源矩形属性

在之前版本的代码中,我们没有帧动画粒子。因此,我们不需要声明一个源矩形。如果您想要将整个纹理渲染到画布上,您可以在调用SDL_RenderCopy时传入NULL来代替源矩形,这就是我们正在做的。现在我们有了帧动画,我们将把渲染到画布上的纹理部分的位置和尺寸传递进来。因此,我们需要定义一个源矩形属性:

SDL_Rect m_src = {.x = 0, .y = 0, .w = 0, .h = 0 };

附加构造函数参数

现在我们已经完成了所有新属性,我们将简要讨论函数签名所需的更改。Particle类构造器必须添加一些新的参数来支持我们的对齐旋转、颜色修改和帧动画功能。下面是构造函数的新签名:

Particle( SDL_Texture *sprite, Uint32 life_time, float acceleration,
             bool alpha_fade, int width, int height, bool align_rotation,
             Uint32 start_color,
             Uint32 end_color,
             Uint32 animation_frames );

名为align_rotationboolean值告诉构造函数将粒子的旋转与其移动方向对齐。如果我们使用粒子系统的新颜色修改功能,start_colorend_color参数是颜色修改值。最后一个参数animation_frames,告诉粒子系统它是否使用帧动画系统,如果是,它将使用多少帧。

更新函数的参数

Update函数签名的修改反映了我们需要对构造函数进行的修改。共有四个新参数用于影响对齐旋转、颜色修改系统和帧动画系统。

下面是新的Update函数签名的样子:

void Update( Uint32 life_time, float acceleration,
             bool alpha_fade, bool align_rotation,
             Uint32 start_color, Uint32 end_color,
             Uint32 m_animation_frames );

产卵函数的参数

最后一个需要修改的函数签名是Spawn函数。当我们生成单个粒子时,需要新的值来允许Emitter设置比例和旋转值。当我们生成粒子时,float start_scalefloat end_scale参数用于设置开始和结束比例乘数。最后一个添加的参数是float rotation,它代表粒子基于这个特定粒子的 xy 速度移动的角度。以下是该功能的新版本:

void Spawn( float x, float y, float velocity_x, float velocity_y,
             float start_scale, float end_scale, float rotation );

换成

我们需要对Particle类进行的下一组更改都是对我们在particle.cpp文件中定义的函数进行的更改。跟踪对这些功能所做的更改很有挑战性,因此,与其讨论这些更改,我将带您了解我们讨论的每个功能中正在发生的一切。

粒子构造器逻辑

新的Particle构造器中的逻辑增加了大量代码,为我们的新特性奠定了基础。以下是该函数的最新版本:

Particle::Particle( SDL_Texture *sprite_texture, Uint32 life_time, 
                   float acceleration, bool alpha_fade, int width, 
                   int height, bool align_rotation,
                   Uint32 start_color, Uint32 end_color, 
                   Uint32 animation_frames ) {

    if( start_color != 0xffffff || end_color != 0xffffff ) {
        m_color_mod = true;
        m_start_red = (Uint8)(start_color >> 16);
        m_start_green = (Uint8)(start_color >> 8);
        m_start_blue = (Uint8)(start_color);

        m_end_red = (Uint8)(end_color >> 16);
        m_end_green = (Uint8)(end_color >> 8);
        m_end_blue = (Uint8)(end_color);

        m_current_red = m_start_red;
        m_current_green = m_start_green;
        m_current_blue = m_start_blue;
    }
    else {
        m_color_mod = false;

        m_start_red = (Uint8)255;
        m_start_green = (Uint8)255;
        m_start_blue = (Uint8)255;

        m_end_red = (Uint8)255;
        m_end_green = (Uint8)255;
        m_end_blue = (Uint8)255;

        m_current_red = m_start_red;
        m_current_green = m_start_green;
        m_current_blue = m_start_blue;
    }
    m_align_rotation = align_rotation;
    m_animation_frames = animation_frames;
    m_sprite_texture = sprite_texture;
    m_life_time = life_time;
    m_acceleration = acceleration;
    m_alpha_fade = alpha_fade;
    m_width = (float)width;
    m_height = (float)height;

    m_src.w = m_dest.w = (int)((float)width / (float)m_animation_frames);
    m_src.h = m_dest.h = height;

    m_next_frame_ms = m_life_time / m_animation_frames;
    m_current_frame = 0;
    m_active = false;
}

该代码的第一个大批量用于在粒子寿命的开始和结束时设置 8 位颜色通道。如果开始颜色或结束颜色不是0xffffff(白色),我们将使用>>操作符(移位)设置开始和结束颜色通道。下面是设置起始通道的代码:

m_start_red = (Uint8)(start_color >> 16);
m_start_green = (Uint8)(start_color >> 8);
m_start_blue = (Uint8)(start_color);

如果不熟悉右移位运算符>>,它取运算符左侧的整数,移位运算符右侧的位数。例如,向右移动两位的二进制值 15 (0000 1111)将返回新值 3 (0000 0011)。当我们向右移动时,任何移动到右侧的位都会丢失,值为 0 的位会从左侧移入:

Figure 9.1: Example of a right bit shift

如果我们有一个 RGB 整数,每个通道占用 1 字节或 8 位。所以,如果 R = 9 ,G = 8 ,B = 7 ,我们十六进制的整数值会是这样的:ff090807。如果我们想得到 R 值,我们需要去掉这个 4 字节整数右边的两个字节。每个字节是 8 位,所以我们将取我们的 RGB 并使用>>运算符将其移位 16 位。然后我们会有值09,我们可以用它来设置我们的 8 位红色通道。当我们使用绿色通道时,我们希望从右边数第二个字节,这样我们就可以移出 8 位。现在,在我们的 4 字节整数中,我们会有 00000908。因为我们将它转换成一个 8 位整数,所有不在最右边字节的数据都会在赋值中丢失,所以我们在绿色通道中以08结束。最后,蓝色通道值已经在最右边的字节中。我们需要做的就是将其转换为一个 8 位整数,这样我们就丢失了所有不在蓝色通道中的数据。以下是 32 位颜色的示意图:

Figure 9.2: Color bits in a 32-bit integer

我们必须在最终颜色通道上执行同样的魔法:

m_end_red = (Uint8)(end_color >> 16);
m_end_green = (Uint8)(end_color >> 8);
m_end_blue = (Uint8)(end_color);

我们要做的最后一件事是将当前颜色通道设置为起始颜色通道。我们这样做是为了用颜色的起始值创建粒子。

如果开始和结束颜色都是白色,我们想将颜色 mod 标志设置为false,所以我们不会尝试修改这个粒子上的颜色。我们将所有颜色通道初始化为255。下面是这样做的代码:

else {
    m_color_mod = false;
    m_start_red = (Uint8)255;
    m_start_green = (Uint8)255;
    m_start_blue = (Uint8)255;

    m_end_red = (Uint8)255;
    m_end_green = (Uint8)255;
    m_end_blue = (Uint8)255;

    m_current_red = m_start_red;
    m_current_green = m_start_green;
    m_current_blue = m_start_blue;
}

管理颜色修改的代码之后是一些初始化代码,它根据传递给构造函数的参数设置该对象中的属性变量:

m_align_rotation = align_rotation;
m_animation_frames = animation_frames;
m_sprite_texture = sprite_texture;
m_life_time = life_time;
m_acceleration = acceleration;
m_alpha_fade = alpha_fade;

m_width = (float)width;
m_height = (float)height;

然后,我们根据传入的高度和宽度以及粒子的动画帧数来设置源矩形和目标矩形:

m_src.w = m_dest.w = (int)((float)width / (float)m_animation_frames);
m_src.h = m_dest.h = height;

最后两行代码将当前帧初始化为0,并将我们的活动标志初始化为false。所有动画都从第0帧开始,新粒子在产生之前不会被激活。

下面是最后几行代码:

m_current_frame = 0;
m_active = false;

粒子更新逻辑

Particle类的Update功能在每个粒子上运行,这些粒子是由以前的巴布亚新几内亚文件上传创建的。此函数更新构造函数中设置的大多数值。唯一的例外是粒子的宽度和高度尺寸必须保持不变。这是因为构造函数根据上传的图像文件的尺寸设置这些值。我觉得没有必要一步一步地完成这个函数的每一部分,因为它与我们刚刚走过的构造函数非常相似。花点时间看看代码,看看它有多相似:

void Particle::Update( Uint32 life_time, float acceleration, 
                       bool alpha_fade, bool align_rotation,
                       Uint32 start_color, Uint32 end_color, 
                       Uint32 animation_frames ) {
    if( start_color != 0xffffff || end_color != 0xffffff ) {
        m_color_mod = true;

        m_start_red = (Uint8)(start_color >> 16);
        m_start_green = (Uint8)(start_color >> 8);
        m_start_blue = (Uint8)(start_color);

        m_end_red = (Uint8)(end_color >> 16);
        m_end_green = (Uint8)(end_color >> 8);
        m_end_blue = (Uint8)(end_color);

        m_current_red = m_start_red;
        m_current_green = m_start_green;
        m_current_blue = m_start_blue;
    }
     else {
        m_color_mod = false;

        m_start_red = (Uint8)255;
        m_start_green = (Uint8)255;
        m_start_blue = (Uint8)255;

        m_end_red = (Uint8)255;
        m_end_green = (Uint8)255;
        m_end_blue = (Uint8)255;

        m_current_red = m_start_red;
        m_current_green = m_start_green;
        m_current_blue = m_start_blue;
    }

    m_align_rotation = align_rotation;
    m_life_time = life_time;
    m_acceleration = acceleration;
    m_alpha_fade = alpha_fade;
    m_active = false;

    m_current_frame = 0;
    m_animation_frames = animation_frames;
    m_next_frame_ms = m_life_time / m_animation_frames;;

    m_src.w = m_dest.w = (int)((float)m_width / (float)m_animation_frames);
    m_src.h = m_dest.h = m_height;
}

粒子产卵函数

每当Emitter需要发射新粒子时,它就会运行Particle类的Spawn功能。当发射器到达下一个粒子发射时间时,它会搜索粒子池,寻找标记为未激活的粒子。如果它找到一个粒子,它调用该粒子上的Spawn函数,激活该粒子并设置几个特定于其运行的值。每次发射粒子时,传递到Spawn的所有值都会被Emitter改变。下面是这个函数的代码:

void Particle::Spawn( float x, float y,
                      float velocity_x, float velocity_y,
                      float start_scale, float end_scale,
                      float rotation ) {
     m_position.x = x;
     m_dest.x = (int)m_position.x;
     m_position.y = y;
     m_dest.y = (int)m_position.y;

    m_velocity.x = velocity_x;
    m_velocity.y = velocity_y;

    m_alpha = 255.0;
    m_active = true;
    m_ttl = m_life_time;
    m_rotation = rotation;

    m_current_red = m_start_red;
    m_current_green = m_start_green;
    m_current_blue = m_start_blue;

    m_current_scale = m_start_scale = start_scale;
    m_end_scale = end_scale;

    m_current_frame = 0;
    m_next_frame_ms = m_life_time / m_animation_frames;
}

这个函数中所做的几乎所有事情都是初始化,非常简单。前四行初始化位置属性(m_position,以及带有目标矩形(m_dest)的位置。然后,设定速度。阿尔法总是从255开始。粒子被激活,生存时间变量被激活,旋转被设置。颜色通道被重新初始化,刻度被初始化,当前帧和到下一帧的时间被设置。

粒子移动功能

Particle类的Move函数是一个不仅可以改变粒子渲染位置,还可以调整粒子生命开始和结束之间所有插值的函数。让我们逐步了解一下代码:

void Particle::Move() {
    float time_pct = 1.0 - (float)m_ttl / (float)m_life_time;
    m_current_frame = (int)(time_pct * (float)m_animation_frames);
    float acc_adjusted = 1.0f;

    if( m_acceleration < 1.0f ) {
        acc_adjusted = 1.0f - m_acceleration;
        acc_adjusted *= delta_time;
        acc_adjusted = 1.0f - acc_adjusted;
    }
    else if( m_acceleration > 1.0f ) {
        acc_adjusted = m_acceleration - 1.0f;
        acc_adjusted *= delta_time;
        acc_adjusted += 1.0f;
    }
    m_velocity.x *= acc_adjusted;
    m_velocity.y *= acc_adjusted;

    m_position.x += m_velocity.x * delta_time;
    m_position.y += m_velocity.y * delta_time;

    m_dest.x = (int)m_position.x;
    m_dest.y = (int)m_position.y;

    if( m_alpha_fade == true ) {
         m_alpha = 255.0 * (1.0 - time_pct);
         if( m_alpha < 0 ) {
            m_alpha = 0;
        }
    }
    else {
        m_alpha = 255.0;
    }
    if( m_color_mod == true ) {
        m_current_red = m_start_red + (Uint8)(( m_end_red - m_start_red
        ) * 
        time_pct);
        m_current_green = m_start_green + (Uint8)(( m_end_green -
        m_start_green ) * 
        time_pct);
        m_current_blue = m_start_blue + (Uint8)(( m_end_blue -
        m_start_blue ) * 
        time_pct);
    }

    m_current_scale = m_start_scale + (m_end_scale - m_start_scale) * 
    time_pct;
    m_dest.w = (int)(m_src.w * m_current_scale);
    m_dest.h = (int)(m_src.h * m_current_scale);    
    m_ttl -= diff_time;

    if( m_ttl <= 0 ) {
        m_active = false;
    }
    else {
        m_src.x = (int)(m_src.w * m_current_frame);
    }
}

Move函数的第一行计算time_pct。这是一个浮点值,范围从0.0 - 1.0。当粒子刚刚产生时,该变量以值0.0开始,当粒子准备去激活时,该变量达到1.0。它给我们一个浮点值,指示我们在这个粒子的生命周期中所处的位置:

float time_pct = 1.0 - (float)m_ttl / (float)m_life_time;

m_ttl属性是该粒子的生存时间,单位为毫秒,m_life_time是该粒子的总寿命。这个值对于我们在这个Move函数中进行插值计算非常有用。

下面一行根据time_pct中的值返回当前帧:

m_current_frame = (int)(time_pct * (float)m_animation_frames);

之后,几条线根据加速度值调整粒子的 x 和 y 速度:

float acc_adjusted = 1.0f;

if( m_acceleration < 1.0f ) {
    acc_adjusted = 1.0f - m_acceleration;
    acc_adjusted *= delta_time;
    acc_adjusted = 1.0f - acc_adjusted;
}
else if( m_acceleration > 1.0f ) {
    acc_adjusted = m_acceleration - 1.0f;
    acc_adjusted *= delta_time;
    acc_adjusted += 1.0f;
}

m_velocity.x *= acc_adjusted;
m_velocity.y *= acc_adjusted;

我们需要根据已经过去的几分之一秒(delta_time)将acc_adjusted变量设置为m_acceleration变量的修改版本。更改m_velocity值后,我们需要使用这些速度值来修改粒子的位置:

m_position.x += m_velocity.x * delta_time;
m_position.y += m_velocity.y * delta_time;

m_dest.x = (int)m_position.x;
m_dest.y = (int)m_position.y;

如果m_alpha_fade变量是true,代码将修改阿尔法值,在time_pct值变为1.0时将其内插至0。如果未设置m_alpha_fade标志,阿尔法值将设置为255(完全不透明度)。下面是代码:

if( m_alpha_fade == true ) {
    m_alpha = 255.0 * (1.0 - time_pct);
    if( m_alpha < 0 ) {
        m_alpha = 0;
    }
}
else {
    m_alpha = 255.0;
}

如果m_color_mod标志为true,我们需要使用time_pct在起始通道颜色值和结束通道颜色值之间进行插值,以便找到当前通道颜色值:

if( m_color_mod == true ) {
    m_current_red = m_start_red + (Uint8)(( m_end_red - m_start_red ) *         
    time_pct);
    m_current_green = m_start_green + (Uint8)(( m_end_green -
    m_start_green ) * time_pct);
    m_current_blue = m_start_blue + (Uint8)(( m_end_blue - m_start_blue         
    ) * time_pct);
}

找到每个颜色通道的插值后,我们需要使用time_pct对当前比例进行插值。然后,我们根据当前比例值和源矩形的尺寸设置目标宽度和目标高度:

m_current_scale = m_start_scale + (m_end_scale - m_start_scale) * time_pct;
m_dest.w = (int)(m_src.w * m_current_scale);
m_dest.h = (int)(m_src.h * m_current_scale);

我们要做的最后一件事是将m_ttl变量(生存时间)减少diff_time(自上一帧渲染以来的时间)。如果生存时间下降到或低于0,我们将停用粒子,使其在粒子池中可用,并停止渲染。如果还有一些时间,我们将m_src.x(源矩形 x 值)设置到要渲染的帧的适当位置:

m_ttl -= diff_time;
if( m_ttl <= 0 ) {
    m_active = false;
}
else {
    m_src.x = (int)(m_src.w * m_current_frame);
}

粒子渲染功能

我们Particle类的最后一个函数是Render函数。Emitter类为粒子池中的每个活动粒子调用该函数。该函数设置粒子使用的子画面纹理的 alpha 和颜色通道值。然后检查m_align_rotation标志,看是否需要使用SDL_RenderCopySDL_RederCopyEx将纹理复制到后缓冲区。这两个渲染调用的区别在于SDL_RenderCopyEx允许旋转或翻转副本。这两个函数都使用m_src矩形来确定要复制的纹理内部的矩形。两者都使用m_dest矩形来确定后缓冲区中的目的地,在那里我们复制我们的纹理数据:

void Particle::Render() {

    SDL_SetTextureAlphaMod(m_sprite_texture,
                            (Uint8)m_alpha );

    if( m_color_mod == true ) {
        SDL_SetTextureColorMod(m_sprite_texture,
        m_current_red,
        m_current_green,
        m_current_blue );
    }

    if( m_align_rotation == true ) {
        SDL_RenderCopyEx( renderer, m_sprite_texture, &m_src, &m_dest, 
                            m_rotation, NULL, SDL_FLIP_NONE );
    }
    else {
        SDL_RenderCopy( renderer, m_sprite_texture, &m_src, &m_dest );
    }
}

在下一节中,我们将讨论如何修改我们的Emitter类来适应我们的改进。

修改发射器类

正如我前面提到的,当我们讨论Emitter类时,它管理和发射粒子。在典型的粒子系统中,您可能有许多发射器。在我们的游戏中,我们最终将允许多个发射器,但在这个工具中,为了简单起见,我们将保持单个发射器。我们在Emitter类中定义了四个函数,我们将改变其中的三个。唯一不需要改变的功能是GetFreeParticle功能。如果不记得了,GetFreeParticle循环通过m_particle_pool(粒子池属性)寻找未标记为活动的粒子(particle->m_active == false)。如果它找到一个,它就返回那个粒子。如果不是,则返回null

发射器构造函数

Emitter构造器的代码将需要更改,以允许我们设置支持新粒子系统功能所需的属性。以下是新的Emitter构造函数的代码:

Emitter::Emitter(char* sprite_file, int max_particles, float min_angle, 
         float max_angle, Uint32 particle_lifetime, 
         float acceleration, bool alpha_fade,
         float min_starting_velocity, float max_starting_velocity,
         Uint32 emission_rate, int x_pos, int y_pos, float radius,
         float min_start_scale, float max_start_scale,
         float min_end_scale, float max_end_scale,
         Uint32 start_color, Uint32 end_color,
         float burst_time_pct, Uint32 burst_particles,
         bool loop, bool align_rotation, Uint32 emit_time_ms, 
         Uint32 animation_frames ) {
    m_start_color = start_color;
    m_end_color = end_color;
    m_active = true;
    if( min_starting_velocity > max_starting_velocity ) {
        m_min_starting_velocity = max_starting_velocity;
        m_max_starting_velocity = min_starting_velocity;
    }
    else {
        m_min_starting_velocity = min_starting_velocity;
        m_max_starting_velocity = max_starting_velocity;
    }
    SDL_Surface *temp_surface = IMG_Load( sprite_file );
    if( !temp_surface ) {
        printf("failed to load image: %s\n", IMG_GetError() );
        return;
    }
    m_sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface 
    );
    SDL_FreeSurface( temp_surface );
    SDL_QueryTexture( m_sprite_texture,
                        NULL, NULL,
                        &m_sprite_width, &m_sprite_height );
    m_max_particles = max_particles;
    for( int i = 0; i < m_max_particles; i++ ) {
        m_particle_pool.push_back(
            new Particle( m_sprite_texture, particle_lifetime, 

                          acceleration, alpha_fade, m_sprite_width, 
                          m_sprite_height, align_rotation,
                          m_start_color, m_end_color, 
                          animation_frames )
            );
    }
    m_max_angle = max_angle;
    m_min_angle = min_angle;
    m_radius = radius;
    m_position.x = (float)x_pos;
    m_position.y = (float)y_pos;
    m_emission_rate = emission_rate;
    m_emission_time_ms = 1000 / m_emission_rate;
    m_next_emission = 0;
    /* new values */
    m_min_start_scale = min_start_scale;
    m_max_start_scale = max_start_scale;
    m_min_end_scale = min_end_scale;
    m_max_end_scale = max_end_scale;

    m_loop = loop;
    m_align_rotation = align_rotation;
    m_emit_loop_ms = emit_time_ms;
    m_ttl = m_emit_loop_ms;
    m_animation_frames = animation_frames;
    m_burst_time_pct = burst_time_pct;
    m_burst_particles = burst_particles;
    m_has_burst = false;
}

这段代码已经改变了很多,我觉得遍历整个函数是有意义的。前两行设置color属性,然后通过将m_active设置为true来激活发射器。当发射器被创建或更新时,我们将此激活标志设置为true。如果它是一个循环发射器,活动标志将无限期保持打开。如果Emitter不循环,发射器将在达到其发射时间结束时停止发射,如emit_time_ms参数所设置的。

接下来我们要做的是设定最小和最大启动速度。我们在Emitter中有一个小代码,可以确保max_starting_velocity大于min_starting_velocity,但是当我们将这个代码移动到游戏中时,我们可能会选择将值设置为任何有效的值。下面是代码:

if( min_starting_velocity > max_starting_velocity ) {
    m_min_starting_velocity = max_starting_velocity;
    m_max_starting_velocity = min_starting_velocity;
}
else {
    m_min_starting_velocity = min_starting_velocity;
    m_max_starting_velocity = max_starting_velocity;
}

在我们设置了速度之后,使用sprite_file字符串创建了一个 SDL 表面,这是我们已经加载到网络组件虚拟文件系统中的文件的位置。如果该文件不在虚拟文件系统中,我们将打印出一条错误消息并退出构造函数:

SDL_Surface *temp_surface = IMG_Load( sprite_file );

if( !temp_surface ) {
    printf("failed to load image: %s\n", IMG_GetError() );
    return;
}

从图像文件创建表面后,我们使用该表面创建名为m_sprite_texture的 SDL 纹理,然后使用SDL_FreeSurface销毁表面使用的内存,因为现在我们有了纹理,不再需要它。然后,我们调用SDL_QueryTexture来检索雪碧纹理的宽度和高度,并使用它们来设置Emitter属性m_sprite_widthm_sprite_height。下面是代码:

m_sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );
SDL_FreeSurface( temp_surface );
SDL_QueryTexture( m_sprite_texture,
                  NULL, NULL,
                  &m_sprite_width, &m_sprite_height );

接下来我们需要做的是设置m_max_particles属性,并使用该变量初始化粒子池。一个for循环用于将新粒子推到std::vector变量m_particle_pool的后面:

m_max_particles = max_particles;
for( int i = 0; i < m_max_particles; i++ ) {
    m_particle_pool.push_back(
        new Particle( m_sprite_texture, particle_lifetime, acceleration,
                        alpha_fade, m_sprite_width, m_sprite_height, 
                        align_rotation,
                        m_start_color, m_end_color, animation_frames )
    );
}

设置粒子池后,我们使用参数来设置新旧粒子系统值的发射器属性:

m_max_angle = max_angle;
m_min_angle = min_angle;
m_radius = radius;
m_position.x = (float)x_pos;
m_position.y = (float)y_pos;
m_emission_rate = emission_rate;
m_emission_time_ms = 1000 / m_emission_rate;
m_next_emission = 0;

/* new values */
m_min_start_scale = min_start_scale;
m_max_start_scale = max_start_scale;
m_min_end_scale = min_end_scale;
m_max_end_scale = max_end_scale;

m_loop = loop;
m_align_rotation = align_rotation;
m_emit_loop_ms = emit_time_ms;
m_ttl = m_emit_loop_ms;
m_animation_frames = animation_frames;
m_burst_time_pct = burst_time_pct;
m_burst_particles = burst_particles;
m_has_burst = false;

发射极更新逻辑

EmitterUpdate功能与构造函数类似,但在Emitter已经存在需要更新时运行。该功能从设置我们的Emitter上的所有属性变量开始:

if( min_starting_velocity > max_starting_velocity ) {
    m_min_starting_velocity = max_starting_velocity;
    m_max_starting_velocity = min_starting_velocity;
}
else {
    m_min_starting_velocity = min_starting_velocity;
    m_max_starting_velocity = max_starting_velocity;
}
m_active = true;
m_has_burst = false;
m_max_particles = max_particles;
m_min_angle = min_angle;
m_max_angle = max_angle;
m_emission_rate = emission_rate;
m_emission_time_ms = 1000 / m_emission_rate;
m_position.x = (float)x_pos;
m_position.y = (float)y_pos;
m_radius = radius;
/* new values */
m_min_start_scale = min_start_scale;
m_max_start_scale = max_start_scale;
m_min_end_scale = min_end_scale;
m_max_end_scale = max_end_scale;
m_start_color = start_color;
m_end_color = end_color;
m_burst_time_pct = burst_time_pct;
m_burst_particles = burst_particles;
m_loop = loop;
m_align_rotation = align_rotation;
m_emit_loop_ms = emit_time_ms;
m_ttl = m_emit_loop_ms;
m_animation_frames = animation_frames;

设置属性变量后,我们可能需要增加或减少m_particle_pool向量(粒子池)的大小。如果我们池中的粒子数大于新的最大粒子数,我们可以通过简单的调整大小来缩小粒子池。如果粒子池太小,我们将需要循环创建新粒子的代码,并将这些粒子添加到池中。我们这样做,直到池的大小匹配新的最大粒子数。下面是这样做的代码:

if( m_particle_pool.size() > m_max_particles ) {
    m_particle_pool.resize( m_max_particles );
}
else if( m_max_particles > m_particle_pool.size() ) {
    while( m_max_particles > m_particle_pool.size() ) {
        m_particle_pool.push_back(
            new Particle( m_sprite_texture, particle_lifetime, 
                            acceleration, alpha_fade, m_sprite_width, 
                            m_sprite_height, m_align_rotation,
                            m_start_color, m_end_color, 
                            m_animation_frames )
        );
    }
}

现在我们已经调整了粒子池的大小,我们需要循环该池中的每个粒子,并对每个粒子运行Update函数,以确保每个粒子都用新的属性值更新。下面是代码:

Particle* particle;
std::vector<Particle*>::iterator it;
for( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) {
    particle = *it;
    particle->Update( particle_lifetime, acceleration, alpha_fade, 
    m_align_rotation, m_start_color, m_end_color, m_animation_frames );
}

发射器移动功能

我们需要更新的最后一个发射器功能是Emitter::Move功能。这个函数决定了它在这个帧中是否发出任何新的粒子,如果是,有多少。它还使用随机化来挑选这些粒子的许多起始值,在从我们的 HTML 传入的范围内。生成任何新粒子后,该函数将在粒子池中循环,移动和渲染当前活动的任何粒子。以下是该函数的完整代码:

void Emitter::Move() {
    Particle* particle;
    std::vector<Particle*>::iterator it;
    if( m_active == true ) {
        m_next_emission -= diff_time;
        m_ttl -= diff_time;
        if( m_ttl <= 0 ) {
            if( m_loop ) {
                m_ttl = m_emit_loop_ms;
                m_has_burst = false;
            }
            else {
                m_active = false;
            }
        }
        if( m_burst_particles > 0 && m_has_burst == false ) {
            if( (float)m_ttl / (float)m_emit_loop_ms <= 1.0 - 
            m_burst_time_pct ) {
                m_has_burst = true;
                m_next_emission -= m_burst_particles * m_emission_time_ms;
            }
        }
        while( m_next_emission <= 0 ) {
            m_next_emission += m_emission_time_ms;
            particle = GetFreeParticle();
            if( particle != NULL ) {
                Point spawn_point;
                spawn_point.x = get_random_float( 0.0, m_radius );
                Point velocity_point;
                velocity_point.x = get_random_float( 
                m_min_starting_velocity, m_max_starting_velocity );
                float angle = get_random_float( m_min_angle, m_max_angle );
                float start_scale = get_random_float( m_min_start_scale, 
                m_max_start_scale );
                float end_scale = get_random_float( m_min_end_scale, 
                m_max_end_scale );
                spawn_point.x += m_position.x;
                spawn_point.y += m_position.y;
                particle->Spawn(spawn_point.x, spawn_point.y, 
                velocity_point.x, velocity_point.y,
                                start_scale, end_scale,
                                (int)(angle / 3.14159 * 180.0 + 360.0) 
                                % 360 );
            }
            else {
                m_next_emission = m_emission_time_ms;
            }
        }
    }
    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) {
        particle = *it;
        if( particle->m_active ) {
            particle->Move();
            particle->Render();
        }
    }
}

我们将把这段代码分成两部分,以便更容易理解。Move功能的第一部分负责在必要时产生新的粒子。第二部分负责移动和渲染任何现有的活动粒子。该代码的粒子产生部分仅在m_active(活动标志)为true时运行。第二部分将是双向的。当发射器被停用时,我们不希望发射器产生的所有粒子突然消失。相反,我们希望所有粒子继续移动和渲染,直到它们都被停用。

我们现在将分小块遍历代码来解释所有内容:

if( m_active == true ) {
    m_next_emission -= diff_time;
    m_ttl -= diff_time;
    if( m_ttl <= 0 ) {
        if( m_loop ) {
            m_ttl = m_emit_loop_ms;
            m_has_burst = false;
        }
        else {
            m_active = false;
        }
    }

第一段代码检查m_active属性变量,以确保发射器当前处于活动状态。如果不是,我们可以跳过这个函数产生新粒子的部分。接下来我们要做的是从m_next_emission属性中减去diff_time。当m_next_emission属性命中或低于0时,会产生另一个粒子。我们还从m_ttl中减去diff_time,这是生存时间属性。从m_ttl减去后,我们立即检查m_ttl中的值,看它是否小于或等于0。如果生存时间下降到0以下,我们需要通过查看m_loop属性来检查这是否是一个循环发射器。如果是循环发射器,我们将时间重置为活变量,并将m_has_burst标志设置为false。如果这不是循环发射器,我们通过将m_active设置为false来停用发射器。

以下代码块与使用新的爆发特性发射粒子爆发有关:

if( m_burst_particles > 0 && m_has_burst == false ) {
    if( (float)m_ttl / (float)m_emit_loop_ms <= 1.0 - m_burst_time_pct ) {
        m_has_burst = true;
        m_next_emission -= m_burst_particles * m_emission_time_ms;
    }
}

爆发粒子功能对于我们的高级粒子系统来说是全新的。我们在这里使用嵌套的if语句。我们可以把&&放在第一个if的末尾,用一个if语句来完成,但是我想把条件分开,这样更容易理解。外部if语句首先检查m_burst_particles属性(爆发粒子数)是否大于0。如果是,那么这个发射器使用爆发系统,需要在适当的爆发时间产生粒子爆发。在这个外部if语句中的下一个检查是检查脉冲是否已经在这个发射器中运行。由于我们设计这种突发系统的方式,每个发射回路只能有一个突发。所以,如果m_has_burst属性是true,那么一个爆发就不会运行。

转到内环,我们需要检查我们是否已经过了发射的爆发时间。m_burst_time_pct属性包含一个介于0.01.0之间的值,代表粒子爆发发生时发射时间的十进制百分比。m_ttl变量保存发射器的生存时间,单位为毫秒。如果我们将m_ttl除以m_emit_loop_ms(发射时间,单位为毫秒),我们会得到从1.00.0的发射时间倒计时,其中0.0表示发射完成。m_burst_time_pct变量向另一个方向发展。0.6的值意味着爆发发生在我们发射过程的 60%。因为这个if语句的另一面是倒计时,突发时间也在计数,我们需要从1.0中减去m_burst_time_pct来做一个适当的比较。如果(float)m_ttl / (float)m_emit_loop_ms小于1.0 - m_burst_time_pct,那么我们就做好了爆发的准备。为了让爆发发生,我们首先设置m_has_burst = true。这将防止爆发在同一发射中多次发生。然后我们从m_next_emission中减去爆发粒子的数量,乘以发射时间(毫秒)。

以下几行代码进入while循环,只要下一次发射时间小于0,就会发射粒子。在这段代码的前一个版本中,我们这里有一个if语句,而不是一个循环。这限制了我们的粒子系统每帧发射不超过一个粒子。这可能适用于一些没有爆发模式的简单粒子系统,但是一旦你添加了爆发,你需要能够在一个帧中发射许多粒子。让我们看看这个:

while( m_next_emission <= 0 ) {
    m_next_emission += m_emission_time_ms;
    particle = GetFreeParticle();
    if( particle != NULL ) {

while循环检查m_next_emission是否小于或等于0。紧接其后的线将m_emission_time_ms添加到下一次发射中。这样做的效果是,如果我们从m_next_emission中减去一个大的数字(就像我们在我们的爆发中所做的那样),这个循环将允许我们在一次运行Move函数中发射多个粒子。这意味着我们可以在一个框架内发射大量粒子。添加到m_next_emission后,我们立即通过调用GetFreeParticle从粒子池中获取一个自由粒子。如果我们将最大粒子数设置得太小,GetFreeParticle可能会用完我们可以使用的粒子并返回NULL。如果是这种情况,我们需要跳过发出新粒子的所有步骤,这就是为什么有if语句,它检查NULL粒子。

一旦我们知道我们可以产生一个粒子,我们需要在 HTML 文件中设置的范围内抓取一堆随机值。C/C++ rand()函数返回一个随机整数。我们需要的大部分数值都是浮点。我们需要编写一个名为get_random_float的简单函数。该函数获取一个随机浮点数,其三位小数精度介于传递给它的最小值和最大值之间。我们选择三位小数精度是基于我们对这个游戏的需求。如果以后有必要,可以修改函数以获得更高的精度。

下面是获取随机值以用于新产生的粒子的代码:

Point spawn_point;
spawn_point.x = get_random_float( 0.0, m_radius );
Point velocity_point;
velocity_point.x = get_random_float( m_min_starting_velocity, m_max_starting_velocity );
float angle = get_random_float( m_min_angle, m_max_angle );
float start_scale = get_random_float( m_min_start_scale, m_max_start_scale );
float end_scale = get_random_float( m_min_end_scale, m_max_end_scale );

我们在这里得到的随机值是距离我们的发射器的距离,我们将在这里生成粒子,粒子的速度,粒子的方向角度,以及开始和结束的比例值。因为我们希望从发射器中心以给定角度产生的粒子也具有相同的方向速度,所以我们只给spawn_pointvelocity_pointx 值分配了一个随机数。我们将使用之前随机生成的相同角度来旋转这两个点。以下是这些点的旋转代码:

velocity_point.Rotate(angle);
spawn_point.Rotate( angle );

我们生成相对于0,0原点的位置的种子点。因为我们的发射器可能不在0,0上,我们需要通过m_position点的值来调整产卵点的位置。下面是我们用来做这件事的代码:

spawn_point.x += m_position.x;
spawn_point.y += m_position.y;

我们做的最后一件事是用我们随机生成的值生成粒子:

particle->Spawn(spawn_point.x, spawn_point.y, velocity_point.x, 
                velocity_point.y,
                start_scale, end_scale,
                (int)(angle / 3.14159 * 180.0 + 360.0) % 360 );

现在该函数已经完成了当前帧的粒子生成,该函数将需要在粒子池中循环寻找要移动和渲染的活动粒子:

for( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) {
    particle = *it;
    if( particle->m_active ) {
        particle->Move();
        particle->Render();
    }
}

在下一节中,我们将更新我们从 JavaScript 调用的 C++/WebAssembly 函数。

外部功能

我们正在编写的高级粒子系统有两个外部函数,可以从我们应用中的 JavaScript 调用。调用这些函数add_emitterupdate_emitter来插入或修改网络组件模块中的粒子系统。advanced_particle.cpp文件包含这些函数,以及加载Module时调用的main函数和每帧渲染调用一次的show_emission函数。我们不需要修改本章前面为基本粒子系统创建的mainshow_emission函数。然而,我们需要将我们放入 JavaScript 代码中的附加参数添加到add_emitterupdate_emitter中。此外,我们还创建了一个名为get_random_float的实用函数,我们在生成粒子时使用它。因为这个文件包含了我们所有其他的 C 风格函数,所以我觉得advanced_particle.cpp也是放这个函数最好的地方。

随机浮点数

让我们从讨论新的get_random_float功能开始。下面是代码:

float get_random_float( float min, float max ) {
    int int_min = (int)(min * 1000);
    int int_max = (int)(max * 1000);
    if( int_min > int_max ) {
        int temp = int_max;
        int_max = int_min;
        int_min = temp;
    }
    int int_diff = int_max - int_min;
    int int_rand = (int_diff == 0) ? 0 : rand() % int_diff;
    int_rand += int_min;
    return (float)int_rand / 1000.0;
}

%(模运算符)用于使随机整数值介于 0 和您在%之后使用的任何值之间。模运算符是余数运算符。它返回除法运算的余数。例如,13 % 10会返回 3,23 % 10也会。取任意数的% 10总是得到一个 0 到 9 之间的数。模与rand()一起使用很有用,因为它会产生一个介于 0 和%之后的值之间的随机数。所以,rand() % 10会产生一个 0 到 9 之间的随机数。

get_random_float函数接受最小和最大浮点值,并在该范围内生成一个随机数。前两行接受这些浮点值,将其乘以 1,000,并将其转换为整数。因为rand()只对整数起作用,所以我们需要模拟一个精度值。乘以 1000 可以得到三位小数的精度。例如,如果我们想要生成一个介于 1.125 和 1.725 之间的随机数,这两个值将乘以 1,000,我们将使用rand()生成一个介于 1,125 和 1,175 之间的随机值:

int int_min = (int)(min * 1000);
int int_max = (int)(max * 1000);

rand()再次只生成随机整数,使用rand()旁边的%(模运算符)会给你一个介于0和跟在%后面的数字之间的数字。正因为如此,我们想知道我们的int_minint_max值之间的区别。如果我们从int_max中减去int_min,我们会得到一个就是这个差的数。如果调用代码不小心传入了一个小于int_min的 max 值,我们可能会被抛出,所以我们需要一点代码来检查max是否小于min,如果是,我们需要切换这两个值。下面是if语句代码:

if( int_min > int_max ) {
    int temp = int_max;
    int_max = int_min;
    int_min = temp;
}

现在,我们可以继续了解两者之间的区别:

int int_diff = int_max - int_min;

在下面一行代码中,我们得到一个介于 0 和int_diff中的值之间的随机值。在执行rand() % int_diff之前,我们使用?:(三元运算符)确保int_diff不是 0。这是因为%是一个除法余数运算符,所以像除以 0 一样,执行% 0会导致异常。如果我们的最小值和最大值之间没有差异,我们将返回最小值。所以,如果int_diff为 0,我们可以通过使用三元运算符将int_rand设置为 0。下面是代码:

int int_rand = (int_diff == 0) ? 0 : rand() % int_diff;

然后,我们将int_min加到int_rand上,在int_minint_max值之间有一个随机值:

int_rand += int_min;

我们要做的最后一件事是将int_rand铸造成float并除以1000.0。这将返回一个介于传递给函数的minmax浮点值之间的浮点值:

return (float)int_rand / 1000.0;

添加发射器

add_emitter功能是一个传递,检查是否存在发射器,如果存在,则删除发射器。然后它创建一个新的Emitter对象,传入我们在 HTML 中设置并在 JavaScript 中传递的所有值。我们需要做的更改包括将新参数添加到add_emitter函数的签名中,并将这些相同的新参数添加到对Emitter构造函数的调用中。在函数签名和构造函数调用中,我们将添加一个/* new parameters */注释,显示旧参数的结束和新参数的开始。以下是新代码:

extern "C"
    EMSCRIPTEN_KEEPALIVE
    void add_emitter(char* file_name, int max_particles, float min_angle, 
         float max_angle,
         Uint32 particle_lifetime, float acceleration, bool alpha_fade,
         float min_starting_velocity, float max_starting_velocity,
         Uint32 emission_rate, float x_pos, float y_pos, float radius,
         /* new parameters */
         float min_start_scale, float max_start_scale,
         float min_end_scale, float max_end_scale,
         Uint32 start_color, Uint32 end_color,
         float burst_time_pct, Uint32 burst_particles,
         bool loop, bool align_rotation, Uint32 emit_time_ms,
         Uint32 animation_frames ) {
        if( emitter != NULL ) {
            delete emitter;
        }

        emitter = new Emitter(file_name, max_particles, min_angle, 
                  max_angle,
                  particle_lifetime, acceleration, alpha_fade,
                  min_starting_velocity, max_starting_velocity,
                  emission_rate, x_pos, y_pos, radius,
                  /* new parameters */
                  min_start_scale, max_start_scale,
                  min_end_scale, max_end_scale,
                  start_color, end_color,
                  burst_time_pct, burst_particles,
                  loop, align_rotation, emit_time_ms,
                  animation_frames
                  );
    }

更新发射器

我们对update_emitter功能所做的更改反映了在add_emitter功能中所做的更改。add_emitterupdate_emitter的主要区别在于如果没有现有发射器update_emitter将不会运行,并且它不会调用Emitter构造函数来创建新的Emitter,而是调用现有发射器的Update函数。Update功能传入所有新值和大部分旧值(除了char* file_name)。就像我们对add_emitter函数所做的更改一样,我们在函数签名和对发射器Update函数的调用中放置了一个/* new parameters */注释,以显示新参数添加到了哪里。下面是代码:

extern "C"
    EMSCRIPTEN_KEEPALIVE
    void update_emitter(int max_particles, float min_angle, 
         float max_angle,
         Uint32 particle_lifetime, float acceleration, bool alpha_fade,
         float min_starting_velocity, float max_starting_velocity,
         Uint32 emission_rate, float x_pos, float y_pos, float radius,
         /* new parameters */
         float min_start_scale, float max_start_scale,
         float min_end_scale, float max_end_scale,
         Uint32 start_color, Uint32 end_color,
         float burst_time_pct, Uint32 burst_particles,
         bool loop, bool align_rotation, Uint32 emit_time_ms,
         Uint32 animation_frames ) {
         if( emitter == NULL ) {
                        return;
                    }
                    emitter->Update(max_particles, min_angle, max_angle,
                          particle_lifetime, acceleration, alpha_fade,
                          min_starting_velocity, max_starting_velocity,
                          emission_rate, x_pos, y_pos, radius,
                          /* new parameters */
                          min_start_scale, max_start_scale,
                          min_end_scale, max_end_scale,
                          start_color, end_color,
                          burst_time_pct, burst_particles,
                          loop, align_rotation, emit_time_ms,
                          animation_frames
                    );
                }

在下一节中,我们将配置我们的高级粒子系统工具来创建一个新的粒子发射器

配置粒子发射器

在这一点上,你可能想知道我们什么时候才能继续写游戏。我们构建这个粒子发射器配置工具有几个原因。首先,在编译代码中很难配置粒子系统。如果我们想测试一个发射器的配置,我们需要在每次测试中重新编译我们的值,或者我们需要编写一个数据加载器,并在进行配置更改后重新运行游戏。创建一个工具,允许我们测试不同的发射器配置,允许更快(和更有趣)的粒子系统创建。

HTML 外壳和 WebAssembly 模块交互

我创建粒子系统配置工具也是别有用心的。可能你们中的一些人并不是为了学习游戏编程而阅读这本书。您可能已经购买了这本书,作为了解更多关于 WebAssembly 的有趣方式。编写这个工具是一种有趣的方式,可以了解更多关于 WebAssembly 模块和驱动该模块的 HTML 和 JavaScript 之间的交互。

编译和运行新工具

现在我们已经有了想要的所有参数,是时候重新编译配置工具的更新版本,并开始设计一些粒子系统了。

If you are building this from the GitHub project, you will need to run this compile command from the /Chapter09/advanced-particle-tool/ directory.

首先,在命令行上运行以下命令来编译新的配置工具:

em++ emitter.cpp particle.cpp point.cpp advanced_particle.cpp -o particle.html -std=c++ 17 --shell-file advanced_particle_shell.html -s NO_EXIT_RUNTIME=1 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] -s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS="['_add_emitter', '_update_emitter', '_main']" -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall']" -s FORCE_FILESYSTEM=1

emrun或网络浏览器中打开网页(如果您运行的是网络服务器)。它看起来像这样:

Figure 9.3: Screenshot of our particle system configuration tool

我们将从一个简单的废气排放器开始。对 HTML 值进行以下更改,然后单击上传。png 按钮:

  • 最小角度:-10°
  • 最大角度:10°
  • 最大粒子数:500
  • 排放率:50
  • 半径:0.5
  • 我的开局不错:100.0
  • 最大启动井数:150.0
  • 爆发时间:0.7
  • 爆裂颗粒:40
  • 动画帧数:6

单击上传后。png 按钮,导航到图像目录中的ProjectileExpOrange.png文件并打开。

下面是配置工具与我们的排气粒子发射器的截图:

Figure 9.4: Engine exhaust configuration

我鼓励你玩弄价值观,直到你得到你喜欢的东西。每当您更改页面左侧的值时,您都需要单击“更新发射器”按钮,以查看新值在网页右侧的粒子系统中的反映。

创建粒子发射器

现在我们有了一个排气粒子系统,我们将开始在游戏中添加粒子系统代码,以添加一些不错的粒子效果。我想要一个玩家和敌舰排气的粒子系统。我还想在动画爆炸的顶部添加一个粒子系统效果,我们必须让它脱颖而出。

我们要做的第一件事是将particle.cppemitter.cpp文件复制到主Chapter09目录中。之后,我们需要将这些类定义添加到game.hpp文件以及get_random_float函数原型中。

对 game.hpp 的更改

我们需要做的第一组更改是对game.hpp文件进行更改。我们需要为get_random_float添加一个Emitter类定义、Particle类定义和一个外部函数原型。我们还需要给Ship类增加一些新的属性。以下是我们必须为get_random_float原型添加的线:

extern float get_random_float( float min, float max );

添加粒子类定义

我们必须添加到game.hpp中的Particle类的定义与我们的高级配置工具的定义相同。因为都是一样的,我们就不走班里什么都做了。如果你不记得了,请随时回到上一章作为参考。这是我们将要添加到game.hpp中的Particle的类定义代码:

class Particle {
    public:
        bool m_active;
        bool m_alpha_fade;
        bool m_color_mod;
        bool m_align_rotation;

        Uint8 m_start_red;
        Uint8 m_start_green;
        Uint8 m_start_blue;

        Uint8 m_end_red;
        Uint8 m_end_green;
        Uint8 m_end_blue;

        Uint8 m_current_red;
        Uint8 m_current_green;
        Uint8 m_current_blue;

        SDL_Texture *m_sprite_texture;
        int m_ttl;

        Uint32 m_life_time;
        Uint32 m_animation_frames;
        Uint32 m_current_frame;

        Uint32 m_next_frame_ms;
        float m_rotation;
        float m_acceleration;
        float m_alpha;

        float m_width;
        float m_height;

        float m_start_scale;
        float m_end_scale;
        float m_current_scale;

        Point m_position;
        Point m_velocity;
        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };
        SDL_Rect m_src = {.x = 0, .y = 0, .w = 0, .h = 0 };

        Particle( SDL_Texture *sprite, Uint32 life_time, float 
                    acceleration,
                    bool alpha_fade, int width, int height, bool 
                    align_rotation,
                    Uint32 start_color,
                    Uint32 end_color,
                    Uint32 animation_frames );

        void Update( Uint32 life_time, float acceleration,
                    bool alpha_fade, bool align_rotation,
                    Uint32 start_color, Uint32 end_color,
                    Uint32 m_animation_frames );

        void Spawn( float x, float y, float velocity_x, float velocity_y,
                    float start_scale, float end_scale, float rotation );
        void Move();
        void Render();
};

发射器类别定义

Emitter类有一些我们已经添加的附加属性,帮助Emitter相对于游戏对象定位自己。在粒子发射器配置工具中有一个我们不需要的Run功能,但是我们会在游戏代码中需要它,这样我们就可以随时触发EmitterEmitterParticle里面的Update功能在游戏里面不是必须的,但是为了不使改动复杂化,我们将把它们留在里面。Emscripten 死代码消除逻辑应该在编译游戏时移除这些代码。下面是我们需要添加到games.hpp中的Emitter类定义的新代码:

class Emitter {
    public:
        bool m_loop;
        bool m_align_rotation;
        bool m_active;
        bool m_has_burst;

        SDL_Texture *m_sprite_texture;
        std::vector<Particle*> m_particle_pool;
        int m_sprite_width;
        int m_sprite_height;
        int m_ttl;

        // added ----------------------------
        int m_x_adjustment = 0;
        int m_y_adjustment = 0;
        // ----------------------------------

        Uint32 m_max_particles;
        Uint32 m_emission_rate;
        Uint32 m_emission_time_ms;

        Uint32 m_start_color;
        Uint32 m_end_color;

        Uint32 m_burst_particles;
        Uint32 m_emit_loop_ms;
        Uint32 m_animation_frames;

        int m_next_emission;

        float* m_parent_rotation;

        float m_max_angle;
        float m_min_angle;
        float m_radius;
        float m_min_starting_velocity;
        float m_max_starting_velocity;

        float m_min_start_scale;
        float m_max_start_scale;
        float m_min_end_scale;
        float m_max_end_scale;
        float m_min_start_rotation;
        float m_max_start_rotation;
        float m_burst_time_pct;

        // added ----------------------------
        float* m_parent_rotation_ptr;
        float* m_parent_x_ptr;
        float* m_parent_y_ptr;
        // -----------------------------------

        Point m_position;

        Emitter(char* sprite_file, int max_particles, float min_angle, 
              float max_angle,
              Uint32 particle_lifetime, float acceleration, 
              bool alpha_fade,
              float min_starting_velocity, float max_starting_velocity,
              Uint32 emission_rate, int x_pos, int y_pos, float radius,
              float min_start_scale, float max_start_scale,
              float min_end_scale, float max_end_scale,
              Uint32 start_color, Uint32 end_color,
              float burst_time_pct, Uint32 burst_particles,
              bool loop, bool align_rotation,
              Uint32 emit_time_ms, Uint32 animation_frames );

        void Update(int max_particles, float min_angle, float max_angle,
             Uint32 particle_lifetime, float acceleration, bool alpha_fade,
             float min_starting_velocity, float max_starting_velocity,
             Uint32 emission_rate, int x_pos, int y_pos, float radius,
             float min_start_scale, float max_start_scale,
             float min_end_scale, float max_end_scale,
             Uint32 start_color, Uint32 end_color,
             float burst_time_pct, Uint32 burst_particles,
             bool loop, bool align_rotation, Uint32 emit_time_ms,
             Uint32 animation_frames );

        void Move();
        Particle* GetFreeParticle();

        void Run(); // added
 };

我们添加到粒子系统配置工具的代码被标注为added的注释包围。让我来介绍一下这些新属性和新函数的功能。以下是前两个添加的属性:

int m_x_adjustment = 0;
int m_y_adjustment = 0;

这两个值是调整值,用于修改发射器产生粒子的位置。这些变量对于粒子位置相对于发射器跟随的对象位置的小调整非常有用。以下是我们添加的三个属性:

float* m_parent_rotation_ptr;
float* m_parent_x_ptr;
float* m_parent_y_ptr;

这些是指向父对象的 x、y 和旋转属性的指针。例如,如果我们设置Emitter->m_parent_rotation_ptr = &m_Rotation,指针将指向父对象的旋转,我们将能够访问Emitter内部的那个值来调整旋转。m_parent_x_ptrm_parent_y_ptr也是如此。

最后,我们增加了一个Run功能:

void Run();

此功能允许不循环的粒子发射器重新启动。我们将把它用于我们添加到Ship类的Explosion发射器。

更改发射器. cpp

现在我们已经完成了我们需要对game.hpp进行的更改,我们将逐个功能地完成我们将对emitter.cpp文件进行的所有更改。

对构造函数的更改

要对构造函数进行两项更改。首先,我们将在顶部添加一些初始化,初始化所有指向NULL的新指针。我们不需要在每个发射器中使用这些指针,因此我们可以对照NULL检查它们何时被使用或未被使用。接下来,我们将把传递给构造函数的值从度数修改为弧度。函数如下所示:

Emitter::Emitter(char* sprite_file, int max_particles, float min_angle, 
                float max_angle,
                Uint32 particle_lifetime, float acceleration, bool 
                alpha_fade,
                float min_starting_velocity, float max_starting_velocity,
                Uint32 emission_rate, int x_pos, int y_pos, float radius,
                float min_start_scale, float max_start_scale,
                float min_end_scale, float max_end_scale,
                Uint32 start_color, Uint32 end_color,
                float burst_time_pct, Uint32 burst_particles,
                bool loop, bool align_rotation, Uint32 emit_time_ms, Uint32 
                animation_frames ) {
    // added -----------------------------
    m_parent_rotation_ptr = NULL;
    m_parent_x_ptr = NULL;
    m_parent_y_ptr = NULL;
    // -----------------------------------
    m_start_color = start_color;
    m_end_color = end_color;
    m_active = true;

    if( min_starting_velocity > max_starting_velocity ) {
        m_min_starting_velocity = max_starting_velocity;
        m_max_starting_velocity = min_starting_velocity;
    }
    else {
        m_min_starting_velocity = min_starting_velocity;
        m_max_starting_velocity = max_starting_velocity;
    }
    SDL_Surface *temp_surface = IMG_Load( sprite_file );

    if( !temp_surface ) {
        printf("failed to load image: %s\n", IMG_GetError() );
        printf("failed sprite file: %s\n", sprite_file );
        return;
    }
    m_sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface 
    );
    SDL_FreeSurface( temp_surface );
    SDL_QueryTexture( m_sprite_texture,
                        NULL, NULL,
                        &m_sprite_width, &m_sprite_height );
                        m_max_particles = max_particles;

    for( int i = 0; i < m_max_particles; i++ ) {
        m_particle_pool.push_back(
            new Particle( m_sprite_texture, particle_lifetime, 
            acceleration,
                            alpha_fade, m_sprite_width, m_sprite_height, 
                            align_rotation,
                            m_start_color, m_end_color, animation_frames )
            );
    }

    // modified -----------------------------
    m_min_angle = (min_angle+90) / 180 * 3.14159;
    m_max_angle = (max_angle+90) / 180 * 3.14159;
    // --------------------------------------

    m_radius = radius;
    m_position.x = (float)x_pos;
    m_position.y = (float)y_pos;
    m_emission_rate = emission_rate;
    m_emission_time_ms = 1000 / m_emission_rate;
    m_next_emission = 0;
    m_min_start_scale = min_start_scale;
    m_max_start_scale = max_start_scale;
    m_min_end_scale = min_end_scale;
    m_max_end_scale = max_end_scale;

    m_loop = loop;
    m_align_rotation = align_rotation;
    m_emit_loop_ms = emit_time_ms;
    m_ttl = m_emit_loop_ms;

    m_animation_frames = animation_frames;
    m_burst_time_pct = burst_time_pct;
    m_burst_particles = burst_particles;
    m_has_burst = false;
}

第一个变化是在这个函数的最顶端,并将我们的新指针属性设置为NULL:

m_parent_rotation_ptr = NULL;
m_parent_x_ptr = NULL;
m_parent_y_ptr = NULL;

稍后我们会检查这些指针是不是NULL,如果不是,我们会用m_parent_rotation_ptr来调整这个发射器的旋转角度。我们将使用m_parent_x_ptr改变发射器的 x 坐标,我们将使用m_parent_y_ptr调整该发射器的 y 坐标。之后,我们有代码修改传递的最小和最大角度,从度到弧度:

m_min_angle = (min_angle+90) / 180 * 3.14159;
m_max_angle = (max_angle+90) / 180 * 3.14159;

我们需要这样做的真正原因是,我们正在对传递给发射器的值进行硬编码。如果我们创建了一个数据加载器,我们可以在数据加载时完成这种转换。但是,因为我们直接从粒子发射器配置工具中获取这些值,并将这些值硬编码到对新发射器的调用中,所以我们要么必须记住每次更改这些值时都要自己进行转换,要么必须在构造函数和Update函数中进行转换。

对更新功能的更改

Update函数不太可能在我们的游戏中被调用。Emscripten 的死代码删除过程应该会消除它。然而,我们并没有将其从Emitter类中移除。如果你认为你可以这样称呼它,你可能想改变m_min_anglem_max_angle的初始化,把度数转换成弧度,就像我们在构造函数中做的那样:

m_min_angle = (min_angle+90) / 180 * 3.14159;
m_max_angle = (max_angle+90) / 180 * 3.14159;

添加运行函数

在粒子系统配置工具中,我们不需要Run函数,因为调用Update函数会运行EmitterUpdate功能过于繁琐,无法在我们的游戏中使用。它使用了大量的配置变量,我们在调用函数时可能无法访问这些变量。我们所要做的就是将发射器设置为激活状态,重置生存时间和爆发标志。我们没有调用Update,而是创建了一个小的Run函数来做我们需要的事情:

void Emitter::Run() {
    m_active = true;
    m_ttl = m_emit_loop_ms;
    m_has_burst = false;
}

m_active设置为真将激活发射器,以便在调用Move功能时可以产生新粒子。将m_ttl重置为m_emit_loop_ms确保生存时间不会在下次调用Move功能时自动关闭发射器。设置m_has_burst = false确保,如果发射中的某个地方必须发生粒子爆发,它将运行。

对移动功能的更改

新版本的Move功能将需要能够基于父位置修改其位置,并基于父位置的旋转来旋转其定义的位置。它还需要能够使用m_x_adjustmentm_y_adjustment进行微调。以下是Move的完整新版本:

void Emitter::Move() {
 Particle* particle;
 std::vector<Particle*>::iterator it;
    if( m_active == true ) {
        m_next_emission -= diff_time;
        m_ttl -= diff_time;
        if( m_ttl <= 0 ) {
            if( m_loop ) {
                m_ttl = m_emit_loop_ms;
                m_has_burst = false;
            }
            else { m_active = false; }
        }
        if( m_burst_particles > 0 && m_has_burst == false ) {
            if( (float)m_ttl / (float)m_emit_loop_ms <= 1.0 - 
                m_burst_time_pct ) {
                m_has_burst = true;
                m_next_emission -= m_burst_particles * m_emission_time_ms;
            }
        }
        while( m_next_emission <= 0 ) {
            m_next_emission += m_emission_time_ms;
            particle = GetFreeParticle();
            if( particle != NULL ) {
                Point spawn_point, velocity_point, rotated_position;
                spawn_point.x = get_random_float( 0.0, m_radius );
                velocity_point.x = 
                get_random_float(m_min_starting_velocity, 
                m_max_starting_velocity);
                float angle = get_random_float( m_min_angle,m_max_angle );
                float start_scale = get_random_float(m_min_start_scale, 
                m_max_start_scale);
                float end_scale = get_random_float( m_min_end_scale,
                m_max_end_scale );
                if( m_parent_rotation_ptr != NULL ) {
                    angle += *m_parent_rotation_ptr;
                    rotated_position = m_position;
                    rotated_position.Rotate( *m_parent_rotation_ptr );
                }
                velocity_point.Rotate(angle);
                spawn_point.Rotate( angle );

                if( m_parent_rotation_ptr == NULL ) {
                    spawn_point.x += m_position.x;
                    spawn_point.y += m_position.y;
                    if( m_parent_x_ptr != NULL ) { spawn_point.x += 
                    *m_parent_x_ptr; }
                    if( m_parent_y_ptr != NULL ) { spawn_point.y += 
                    *m_parent_y_ptr; }
                }
                else {
                    spawn_point.x += rotated_position.x;
                    spawn_point.y += rotated_position.y;
                    if( m_parent_x_ptr != NULL ) { spawn_point.x += 
                    *m_parent_x_ptr; }
                    if( m_parent_y_ptr != NULL ) { spawn_point.y += 
                    *m_parent_y_ptr; }
                }
                spawn_point.x += m_x_adjustment;
                spawn_point.y += m_y_adjustment;
                particle->Spawn(spawn_point.x, 
                spawn_point.y,velocity_point.x, velocity_point.y,
                    start_scale, end_scale, (int)(angle / 3.14159 * 180.0 + 
                    360.0) % 360 );
            }
            else {
                m_next_emission = m_emission_time_ms;
            }
        }
    }
    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) 
    {
        particle = *it;
        if( particle->m_active ) {
            particle->Move();
            particle->Render();
        }
    }
}

这些代码的大部分与早期版本相同。让我们来看看不同之处。首先,如果有旋转的父对象,我们需要旋转整个粒子系统。我们将把它用于我们将要添加到宇宙飞船物体中的排气粒子系统。这个排气口必须相对于宇宙飞船定位。要做到这一点,我们需要采取的立场,并旋转它。我们还需要将父对象的旋转添加到现有的发射角度。以下是新代码:

Point rotated_position;

if( m_parent_rotation_ptr != NULL ) {
    angle += *m_parent_rotation_ptr;
    rotated_position = m_position;
    rotated_position.Rotate( *m_parent_rotation_ptr );
}

在顶部,我们创建了一个名为rotated_position的新Point对象。如果m_parent_rotation_ptr不是NULL,我们将该值添加到之前计算的发射角中。我们将通过父代的旋转将m_position的值复制到该位置的rotated_positionRotate中。之后,我们会检查m_parent_rotation_ptr是否不是NULL,如果不是,我们会使用rotated_position相对于父对象的位置来计算发射器的位置。以下是if语句,检查m_parent_rotation_ptr == NULL是否。如果它为空,该if块的第一部分将执行之前会执行的操作。下面是代码:

if( m_parent_rotation_ptr == NULL ) {
    spawn_point.x += m_position.x;
    spawn_point.y += m_position.y;
}

因为if语句是检查m_parent_rotation_ptr == NULL是否存在,所以我们不想使用粒子系统位置的旋转版本。该块默认使用未修改的m_position属性。如果m_parent_rotation_ptr不是NULL,我们将运行以下else块:

else {
    spawn_point.x += rotated_position.x;
    spawn_point.y += rotated_position.y;
}

该代码使用了m_position的旋转版本。接下来我们要看看m_parent_x_ptrm_parent_y_ptr是不是NULL。如果不是,那么我们需要使用这些值将家长的位置添加到spawn_point中。下面是这段代码:

if( m_parent_x_ptr != NULL ) {
    spawn_point.x += *m_parent_x_ptr;
}
if( m_parent_y_ptr != NULL ) {
    spawn_point.y += *m_parent_y_ptr;
}

我们将添加到Move功能的最后一段代码是对产卵点的微调整。有时候,粒子系统在旋转之前需要稍微调整一下,让它们看起来恰到好处。因此,我们添加以下内容:

spawn_point.x += m_x_adjustment;
spawn_point.y += m_y_adjustment;

m_x_adjustmentm_y_adjustment的值默认为0,因此如果您想要使用这些值,需要在创建发射器后的某个时间进行设置。

更改 ship.cpp

接下来我们要做的是修改ship.cpp文件,以利用两个新的粒子发射器。我们想要一个用于飞船排气的粒子发射器,一个用于改善飞船爆炸的粒子发射器。我们需要更改Ship类的构造函数、Ship类的Acceleration函数和Ship类的Render函数。

船舶类的构造函数

Ship类的构造函数改变了Ship类内部的大部分函数。我们不仅要初始化新的属性,我们还需要设置发射器的父值和调整值。下面是构造函数的新代码:

Ship::Ship() : Collider(8.0) {
    m_Rotation = PI;
    m_DX = 0.0;
    m_DY = 1.0;
    m_VX = 0.0;
    m_VY = 0.0;
    m_LastLaunchTime = current_time;
    m_Accelerating = false;
    m_Exhaust = new Emitter((char*)"/sprites/ProjectileExpOrange.png", 200,
                            -10, 10,
                            400, 1.0, true,
                            0.1, 0.1,
                            30, 0, 12, 0.5,
                            0.5, 1.0,
                            0.5, 1.0,
                            0xffffff, 0xffffff,
                            0.7, 10,
                            true, true,
                            1000, 6 );

    m_Exhaust->m_parent_rotation_ptr = &m_Rotation;
    m_Exhaust->m_parent_x_ptr = &m_X;
    m_Exhaust->m_parent_y_ptr = &m_Y;
    m_Exhaust->m_x_adjustment = 10;
    m_Exhaust->m_y_adjustment = 10;
    m_Exhaust->m_active = false;
    m_Explode = new Emitter((char*)"/sprites/Explode.png", 100,
                             0, 360,
                             1000, 0.3, false,
                             20.0, 40.0,
                             10, 0, 0, 5,
                             1.0, 2.0,
                             1.0, 2.0,
                             0xffffff, 0xffffff,
                             0.0, 10,
                             false, false,
                             800, 8 );
    m_Explode->m_parent_rotation_ptr = &m_Rotation;
    m_Explode->m_parent_x_ptr = &m_X;
    m_Explode->m_parent_y_ptr = &m_Y;
    m_Explode->m_active = false;
}

前几行和老版本没什么变化。当我们将m_Accelerating初始化为false时,新的变化就开始了。之后,我们设置排气发射器,首先创建一个新的发射器,然后设置父值和调整值,最后将其设置为非活动状态:

m_Exhaust = new Emitter((char*)"/sprites/ProjectileExpOrange.png", 200,
                        -10, 10,
                        400, 1.0, true,
                        0.1, 0.1,
                        30, 0, 12, 0.5,
                        0.5, 1.0,
                        0.5, 1.0,
                        0xffffff, 0xffffff,
                        0.7, 10,
                        true, true,
                        1000, 6 );

 m_Exhaust->m_parent_rotation_ptr = &m_Rotation;
 m_Exhaust->m_parent_x_ptr = &m_X;
 m_Exhaust->m_parent_y_ptr = &m_Y;
 m_Exhaust->m_x_adjustment = 10;
 m_Exhaust->m_y_adjustment = 10;
 m_Exhaust->m_active = false;

所有传递到Emitter函数的值都直接来自粒子系统配置工具。我们必须手动将它们添加到我们的函数调用中。如果我们在一个大项目上工作,这将不是非常可扩展的。我们可能会让配置工具创建某种数据文件(例如,JSON 或 XML)。但是为了方便起见,我们只是根据配置工具内部的内容对这些值进行了硬编码。不幸的是,这些值的顺序与它们在工具内部出现的顺序不同。您需要查看Emitter构造函数的签名,以确保将值放在正确的位置:

Emitter(char* sprite_file, int max_particles, float min_angle, float max_angle,
        Uint32 particle_lifetime, float acceleration, bool alpha_fade,
        float min_starting_velocity, float max_starting_velocity,
        Uint32 emission_rate, int x_pos, int y_pos, float radius,
        float min_start_scale, float max_start_scale,
        float min_end_scale, float max_end_scale,
        Uint32 start_color, Uint32 end_color,
        float burst_time_pct, Uint32 burst_particles,
        bool loop, bool align_rotation, Uint32 emit_time_ms, Uint32 
        animation_frames );

第一个参数sprite_file是文件在虚拟文件系统中的位置。该文件不会自动包含在您的项目中。您需要确保它位于正确的位置。我们将文件放在sprites目录中,并在运行 Emscripten 时使用以下标志:

 --preload-file sprites

在创建我们的Exhaust发射器之后,我们使用以下代码创建一个Explosion发射器:

m_Explode = new Emitter((char*)"/sprites/Explode.png", 100,
                         0, 360,
                         1000, 0.3, false,
                         20.0, 40.0,
                         10, 0, 0, 5,
                         1.0, 2.0,
                         1.0, 2.0,
                         0xffffff, 0xffffff,
                         0.0, 10,
                         false, false,
                         800, 8 );

m_Explode->m_parent_rotation_ptr = &m_Rotation;
m_Explode->m_parent_x_ptr = &m_X;
m_Explode->m_parent_y_ptr = &m_Y;
m_Explode->m_active = false;

m_Explode发射器的创建类似于m_Exhaust发射器,但是我们根据在粒子发射器配置工具中创建的内容,将不同的值传递到发射器中:

Figure 9.5: Explosion configuration

m_Exhaust发射器一样,我们需要设置所有的父指针变量并关闭发射器。与m_Exhaust不同,我们不需要使用m_x_adjustmentm_y_adjustment属性进行微调。

船舶级加速度函数

我们只希望在船加速时运行废气排放器。为此,我们需要在我们飞船的Accelerate功能中设置一面旗帜。以下是加速功能的新版本:

void Ship::Accelerate() {
    m_Accelerating = true; // added line
    m_VX += m_DX * delta_time;
    m_VY += m_DY * delta_time;
}

唯一的变化是在开头增加了一行,将m_Accelerating设置为true。当我们渲染船时,我们可以检查这个标志,并根据里面的值启动或停止发射器。

“船级”渲染功能

Ship级的最后修改是在飞船的Render功能中。在这个函数中,我们将需要添加移动和渲染两个新粒子系统的代码,以及如果船在加速时打开排气,如果没有加速时关闭排气的代码。以下是新版本的功能:

void Ship::Render() {
    if( m_Alive == false ) {
        return;
    }
    m_Exhaust->Move();
    m_Explode->Move();
    dest.x = (int)m_X;
    dest.y = (int)m_Y;
    dest.w = c_Width;
    dest.h = c_Height;
    src.x = 32 * m_CurrentFrame;
    float degrees = (m_Rotation / PI) * 180.0;
    int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,
                                         &src, &dest,
                                         degrees, NULL, SDL_FLIP_NONE );
    if( return_code != 0 ) {
        printf("failed to render image: %s\n", IMG_GetError() );
    }

    if( m_Accelerating == false ) {
        m_Exhaust->m_active = false;
    }
    else {
        m_Exhaust->m_active = true;
    }
    m_Accelerating = false;
}

看看顶部附近添加的第一个代码块:

m_Exhaust->Move();
m_Explode->Move();

对发射器上的Move函数的调用会移动并渲染粒子系统内部的所有粒子。如果发射器需要的话,它也会产生新的粒子。在该功能的末尾,有处理废气排放器的代码:

if( m_Accelerating == false ) {
    m_Exhaust->m_active = false;
}
else {
    m_Exhaust->m_active = true;
}
m_Accelerating = false;

该代码检查m_Accelerating标志是否为false。如果是,我们就关闭废气排放器。如果船在加速,我们将m_active旗设置为true。我们不调用Run功能,因为我们每一帧都在这么做,而且我们不想在每次循环时都在那个发射器上启动时间。最后一行将m_Accelerating设置为false。我们这样做是因为我们的代码中没有任何地方可以检测到船只何时停止加速。如果飞船正在加速,在我们到达代码中的这一点之前,该标志将被设置回true。如果没有,它将保持设置为false

对弹丸池. cpp 的更改

我们不需要在ProjectilePool类里面改动很多。事实上,我们只需要对一个函数进行两次修改。ProjectilePool级内部的MoveProjectiles功能执行所有射弹和我们两艘船之间的碰撞检测。如果一艘船被摧毁,我们在那艘船上运行m_Explode粒子发射器。这将需要在每艘船的命中测试条件中加入两行新代码。以下是新版本的MoveProjectiles功能:

void ProjectilePool::MoveProjectiles() {
    Projectile* projectile;
    std::vector<Projectile*>::iterator it;
    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); it++ ) {
        projectile = *it;
        if( projectile->m_Active ) {
            projectile->Move();
            if( projectile->m_CurrentFrame == 0 &&
                player->m_CurrentFrame == 0 &&
                ( projectile->HitTest( player ) ||
                    player->CompoundHitTest( projectile ) ) ) {
                player->m_CurrentFrame = 1;
                player->m_NextFrameTime = ms_per_frame;
                player->m_Explode->Run(); // added
                projectile->m_CurrentFrame = 1;
                projectile->m_NextFrameTime = ms_per_frame;
            }
            if( projectile->m_CurrentFrame == 0 &&
                enemy->m_CurrentFrame == 0 &&
                ( projectile->HitTest( enemy ) ||
                    enemy->CompoundHitTest( projectile ) ) ) {
                enemy->m_CurrentFrame = 1;
                enemy->m_NextFrameTime = ms_per_frame;
                enemy->m_Explode->Run(); // added
                projectile->m_CurrentFrame = 1;
                projectile->m_NextFrameTime = ms_per_frame;
            }
        }
    }
}

我添加的两行代码用于调用player->m_Explode->Run();enemy->m_Explode->Run();。当玩家的船或敌人的船与其中一个抛射体相撞并被摧毁时,这些线就会执行。

更改 main.cpp

为了添加排气和爆炸粒子系统,我们需要做的最后一个更改是main.cpp文件。这种变化需要增加一个单一的功能,get_random_float。我们之前讨论过这个函数。这是我们的粒子发射器获取介于最小值和最大值之间的随机浮点值的一种方式。下面是代码:

float get_random_float( float min, float max ) {
    int int_min = (int)(min * 1000);
    int int_max = (int)(max * 1000);
    if( int_min > int_max ) {
        int temp = int_max;
        int_max = int_min;
        int_min = temp;
    }
    int int_diff = int_max - int_min;
    int int_rand = (int_diff == 0) ? 0 : rand() % int_diff;
    int_rand += int_min;
    return (float)int_rand / 1000.0;
}

编译新的粒子系统. html 文件

现在我们已经对文件进行了所有必要的更改,我们可以继续使用 Emscripten 来编译和测试新版本的游戏。

If you are building this from the GitHub project, you will need to run this compile command from the /Chapter09/ directory. The previous compile was done from inside the /Chapter09/advanced-particle-tool/ directory, so make sure that you are in the right place when you run this command; otherwise, it won't have the files it needs to build the game.

从命令行执行以下命令:

em++ collider.cpp emitter.cpp enemy_ship.cpp particle.cpp player_ship.cpp point.cpp projectile_pool.cpp projectile.cpp ship.cpp main.cpp -o particle_system.html --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"]

更进一步

我们不会为配置编写数据导出工具。这一章太长了。当你创建粒子系统的时候,你可以花几乎无限的时间来调整它们。粒子系统可以有大量的配置参数。您甚至可以使用贝塞尔曲线进行移动、旋转和缩放。一些先进的粒子系统具有发射其他粒子的粒子。我们可以给一个粒子系统增加的复杂性是没有限制的,但是我在这本书里可以拥有的页数是有限制的,所以我鼓励你拿着这个系统,加入它,直到你得到你想要的结果。

摘要

恭喜你!你已经读完了一个很长的、充满信息的章节。在最后两章中,我们讨论了什么是粒子系统以及为什么使用它们。我们学习了如何向网络程序集虚拟文件系统添加文件以及如何访问它。我们学习了如何在 HTML 外壳文件和 WebAssembly 模块之间创建更高级的交互。然后,我们构建了一个更高级的粒子发射器配置工具,具有更多的功能。在工具中构建了一些好看的粒子系统后,我们获取了数据和代码,并使用它在我们一直在构建的游戏中构建了两个新的粒子发射器。

在下一章中,我们将讨论并为我们的敌人飞船构建人工智能。