EGE示例程序——花火闪烁的夜晚

专栏:EGE专栏

专栏:EGE示例程序


示例程序下载

花火闪烁的夜晚

站点链接
百度网盘示例一 花火闪烁的夜晚
CSDN示例一 花火闪烁的夜晚 (无需积分)

一、烟花

  在做烟花特效前,先来看看烟花的样子。

在这里插入图片描述

  烟花各式各样,并不是只有一种烟花。而接下来要做的是下面这种比较简单的。烟花在发射后,会拖着一条尾巴快速上升,到达一定高度后快速散开,最后逐渐熄灭。

在这里插入图片描述

二、烟花效果的设计

1. 烟花发射过程

  我们将烟花发射过程分为三个阶段,分别是准备阶段、上升阶段和爆炸阶段。

在这里插入图片描述

  在每个阶段,我们都会给烟花设定一个在某个范围内的随机时间,这样就能够将烟花的发射时间错开,不至于过于整齐而显得比较刻意。
  在准备阶段,烟花尚未点燃,不进行显示。准备阶段结束后进入发射阶段,烟花以一定的初速度上升,并且由于重力和空气阻力等因素速度减小,上升一定时间后爆炸,分成多个粒子向四周散开。粒子散开后会受到重力和空气阻力等因素影响会往下坠,一段时间后熄灭。

2. 烟花的构成

  烟花可以看做由很多个有颜色的粒子组成,每一个粒子都带有各自的速度(矢量)。同一个烟花可以是五颜六色,也可以只有一种颜色。

在这里插入图片描述

  上升过程中,粒子基本都堆在一起,炸开时分散向四面八方,会形成一个球状,受重力和空气阻力影响,粒子向外发射后会逐渐向下方坠落。受粒子本身重量、形状及大小等因素影响,不同种类的烟花在爆炸时,发射出的粒子的运动轨迹并不相同。

  烟花在上升阶段是一个整体,上升到最高点后爆炸分散成大量粒子。粒子具有一定的初速度,在重力、空气阻力等影响下位置和速度都在不断地变化。
  由于粒子的位置和速度并不相同,所以需要分别记录每个粒子的位置和速度信息。

  在本例中,同一个烟花的粒子具有同样大小和颜色,因此并没有单独为每个粒子记录大小和颜色信息。
  实际上也可以将所有粒子分成几部分,每一个部分具有相同的颜色,这样在不必记录过多颜色信息的同时也能达到较好的效果。

在这里插入图片描述

  粒子按如下定义,记录位置和速度信息。

//速度
struct Speed 	
{
	double x, y;
};

//位置
struct Pos
{
	double x, y;
};

//粒子
struct Particle
{
	Pos pos;
	Speed speed;
};

  烟花包含多个粒子,还有发射位置、发射速度以及各阶段的持续时间。粒子的初始位置和射出速度则在爆炸时给出。

在这里插入图片描述

  由此我们定义的烟花类包含以下成员变量:

//烟花
class Fireworks
{
private:
	static const int NUM_PARTICLE = 200;	//烟花包含的粒子数
	static const float particleSpeed;		//爆炸时粒子射出的速率
	
private:
	Particle p[NUM_PARTICLE];	//粒子
	
	color_t color;		//烟花颜色
	
	int waitingTime;	//等待时间
	int riseTime;		//上升时间
	int bloomTime;		//爆炸时间
	
	Pos pos;		//位置(上升阶段)
	Speed speed;	//速度(上升阶段)
	
	......
};

3. 烟花和粒子的位置、速度计算

  本例中粒子只在二维平面内运动,不考虑三维透视。

  烟花在初始化时,会给出一个初始位置和初始速度,后面就需要根据这个位置和速度,配合重力等因素来计算出烟花和粒子的位置。

3.1 位置的计算

  在每一帧中,烟花或粒子都会朝着当前方向移动一段距离。这里的速度是以帧为时间单位的,因此每一帧中,位移都和速度相等:
Δ x = v t = v \Delta x=vt = v Δx=vt=v  下一帧的位置为
x ′ = x + Δ x x' = x + \Delta x x=x+Δx
  烟花发射升空时是一个整体,爆炸时会分散成多个粒子。因此粒子的初始位置可以取烟花爆炸时的位置。

在这里插入图片描述

3.1 速度的计算

  烟花发射升空受到重力和空气阻力的影响,会有向下的加速度, v ′ = v + a t v'=v+at v=v+at  每帧的速度变化量 a a a 调整至合适的数值即可。

  烟花爆炸后,随着粒子逐渐燃烧殆尽,空气阻力对粒子速度的影响也越来越大。因此,每一帧会对粒子速度进行衰减 v ′ = k v v' = kv v=kv  系数 k k k 调整至合适值即可 ( 0 < k ⩽ 1 ) (0 < k \leqslant 1) (0<k1)

4. 烟花及粒子的绘制

  爆炸时散射出去的粒子较小,直接在当前位置上绘制一个像素点即可。烟花升空时的轨迹较粗,可以绘制稍大一点的圆或矩形。

在这里插入图片描述
  烟花升空和粒子向四周散射时都会留下一条长长的轨迹,可以通过对图像进行模糊操作来达到这种拖尾效果。粒子移动后,原来位置上绘制的粒子会随着模糊操作的不断进行而逐渐淡化,最后消失。

  相关函数:圆( fillellipse ), 矩形(bar),模糊滤镜函数 (imagefilter_blurring)。

三、 实现

1. 三个阶段的时间

  整个烟花的过程分为三个阶段:准备阶段发射阶段绽放阶段。分别由下面的三个参数来控制时间长短。

  • delayTime :延迟时间, 烟花发射的时刻是不同的,设置 随机的延迟时间,延迟时间过后烟花发射。
  • riseTime :上升时间,上升时间决定烟花发射多久后会爆炸,越大上升得越高, 设置随机时间来让烟花错开。
  • bloomTime :绽放时间, 绽放后多长时间会消失。

参数是通过不断调整得来的,最后得到一个比较合适的设置。程序中初始化时这三个时间如下所示:

delayTime = rand() % 300 + 20;
riseTime = rand() % 80 + 160;
bloomTime = 160;

2. 发射速度和方向

  下面就是背景图,需要让烟花在地面上释放,而且在地面的不同地方发射,并且控制在一定范围内,发射的方向也需要进行控制。地面高度的是相同的,即y坐标相同,不同的是x坐标。

pos :烟花的位置, 要让烟花在不同的地点发射,可以设置不同的初始值。
Speed speed :烟花上升时的速度。

pos.x = rand() % 450 + 300.0f;
pos.y = GROUND_Y;		//地面位置是580

speed.y = myrand(1.0f) - 3.0f;	//上升速度,根据坐标系需要是负的
speed.x = myrand(0.4f) - 0.2f;	//可稍微倾斜

在这里插入图片描述

3. 烟花绘制的流程

3.1 需要考虑的内容
  • 烟花包含多个粒子,粒子数,爆炸时粒子速度大小,粒子颜色。
  • 设置一些时间变量,用来控制烟花上升的时刻,时长和绽放的时间。
  • 上升速度和位置,需要注意坐标系里y是向下为正。烟花初始随机设定好本次的发射位置,和时间,速度等相关属性,绽放完后重新开始。每次都会先更新烟花位置再绘制。
  • 考虑一下重力因素(有往下的加速度),空气阻力因素(速度衰减),爆炸时的球状散开(速度大小相等,方向不同),可以求x, y分速度。
  • 稍微考虑一下水平动量守恒,做一对速度相反的粒子,因为是随机的,怕有时太随机了偏向一边。
3.2 烟花流程

① 创建烟花。
② 对烟花进行 初始化 ,设置烟花从准备到发射,再到绽放的一整个流程的参数,比如三个时间,爆炸时每个粒子的速度。

  其中粒子爆炸速度比较难计算,因为要设置爆炸成球状,速度大小都是相等的,只是速度的方向不同。随机得到方向后,就 将速度投影到xOy平面上,然后再分别投影到x轴和y轴。 可以参考空间向量如何获得其两个x, y方向的分量。

void Fireworks::init()
{
	delayTime = rand() % 300 + 20;
	riseTime = rand() % 80 + 160;
	bloomTime = 160;

	risePos.x = rand() % 450 + 300.0f;
	risePos.y = GROUND;

	riseSpeed.y = myrand(1.0f) - 3.0f;	//上升速度,根据坐标系需要是负的
	riseSpeed.x = myrand(0.4f) - 0.2f;	//可稍微倾斜

	//随机颜色
	color = HSVtoRGB(myrand(360.0f), 1.0f, 1.0f);

	//给每一个粒子设置初始速度
	for (int i = 0; i < NUM_PARTICLE - 1; i += 2)
	{
		//为了球状散开,设初始速度大小相等
		//初始随机速度水平角度和垂直角度,因为看到是平面的,所以求x, y分速度
		float levelAngle = randomf() * 360;
		float verticalAngle = randomf() * 360;

		//速度投影到xOy平面
		float xySpeed = particleSpeed * cos(verticalAngle);

		//求x, y分速度
		p[i].speed.x = xySpeed * cos(levelAngle);
		p[i].speed.y = xySpeed * sin(levelAngle);

		//动量守恒,每对速度反向
		if (i + 1 < NUM_PARTICLE) {
			p[i + 1].speed.x = -p[i].speed.x;
			p[i + 1].speed.y = -p[i].speed.y;
		}
	}
}

③ 每一帧里对烟花的位置以及粒子的位置进行更新。
  上升时只更新烟花的位置就行了,因为粒子都在一起,爆炸时就要分别计算粒子的位置了。从上升到爆炸的过渡阶段,要给每一个粒子设置位置,因为之前更新的都是烟花的位置,粒子的位置没有单独给出,此时直接将粒子位置设置为烟花当前位置。每次对位置进行更新,都将对应的时间值减1。

阶段的判断:
准备阶段延迟时间 大于0, 不更新位置
上升阶段延迟时间 为 0,并且 上升时间 大于0, 更新烟花位置,如果到达爆炸阶段,就给粒子当前位置。
爆炸阶段 :前面两个时间都不大于0,爆炸时间 大于0, 更新粒子位置
结束阶段 :前面三个时间都为0,这时就对烟花重新初始化

//更新位置等相关属性
void Fireworks::update()
{
	if (delayTime > 0) {
		delayTime--;
		return;
	}
	//处于上升阶段,只更新烟花位置
	else if (riseTime > 0) {
		risePos.x += riseSpeed.x;
		risePos.y += riseSpeed.y;

		//重力作用
		riseSpeed.y += 0.005;

		//上升完毕,到达爆炸阶段
		if (--riseTime <= 0) {
			//设粒子初始位置为烟花当前位置
			for (int i = 0; i < NUM_PARTICLE; i++) {
				p[i].pos.x = risePos.x;
				p[i].pos.y = risePos.y;
			}
		}
	}
	//烟花绽放阶段
	else if (bloomTime > 0) {
		bloomTime--;
		//粒子散开,更新粒子位置
		for (int i = 0; i < NUM_PARTICLE; i++) {
			p[i].pos.x += p[i].speed.x;
			p[i].pos.y += p[i].speed.y;

			//重力作用
			p[i].speed.y += 0.005;

			//速度减慢
			p[i].speed.x *= 0.982;
			p[i].speed.y *= 0.982;
		}
	}
	else {
		//烟花重新开始
		init();
	}
}

④ 每一帧里,如果是准备阶段,不绘制,如果是上升阶段,就根据烟花的位置绘制,如果是爆炸阶段,就根据每一个粒子的位置进行绘制。

void Fireworks::draw(PIMAGE pimg)
{
	//未开始
	if (delayTime > 0)
		return;
	//烟花上升阶段
	else if (riseTime > 0) {
		setfillcolor(color, pimg);
		//画四个点,这样大一些
		bar(risePos.x, risePos.y, risePos.x + 2, risePos.y + 2, pimg);
	}
	//烟花绽放阶段
	else {
		setfillcolor(color, pimg);
		for (int i = 0; i < NUM_PARTICLE; i++) {
			bar(p[i].pos.x, p[i].pos.y, p[i].pos.x + 2, p[i].pos.y + 2, pimg);
		}
	}
}

  最后需要对烟花进行加模糊滤镜,这个是为了加拖尾效果,否则粒子都只是一个小点,不带拖尾。可以通过调整模糊滤镜的参数来改变不同的效果。

4. 程序流程

① 创建烟花,播放背景音乐
② 帧循环中,不断绘制背景于窗口,更新烟花位置, 绘制烟花于缓存,对缓存加模糊滤镜,绘制缓存到窗口。
③ 结束时释放资源

注意事项

  因为涉及模糊滤镜,所以不可能在有背景的窗口加模糊滤镜,这样背景会糊掉,所以只能另外设个图像缓存,在里面做图像模糊操作。

  将图像缓存绘制到窗口时,要特别注意,我们需要的是光叠加的效果,所以绘制时需要到三元光栅操作码 SRCPAINT, 即 目标图像和源图像颜色位或,这个请参考官网源文档,这个码还挺难找的,因为列表里操作码很多。

5. 程序源码

  下面是项目的一些文件:
在这里插入图片描述

其中包括:

  • 源文件:fireworks.cpp, fireworks.h 和 main.cpp
  • 资源文件:花火bg.jpg(图片文件), 羽肿 - 花火が瞬く夜に.mp3(音乐文件)
5.1 fireworks.h

  下面是 fireworks.h 头文件的内容:

#pragma once

#ifndef FIREWORKS_H_
#define FIREWORKS_H_

#include <graphics.h>

//随机数, 0.0~1.0
#define myrand(m) ((float)rand() * m / 36565)

struct Speed {
	double x, y;
};

struct Pos {
	double x, y;
};

struct Particle
{
	Pos pos;
	Speed speed;
};


#define GROUND 580	//地面位置y坐标

class Fireworks//烟花类			
{
private:

	static const int NUM_PARTICLE = 200;
	static const double particleSpeed;
	Particle p[NUM_PARTICLE];

	color_t color;

	int delayTime;		//延迟时间
	int riseTime;		//上升时间
	int bloomTime;		//爆炸时间

	Pos risePos;		//上升阶段位置
	Speed riseSpeed;	//上升速度

public:
	//初始化
	Fireworks();

	void init();

	//更新位置等相关属性
	void update();

	//根据属性值绘画
	void draw(PIMAGE pimg = NULL);
};

#endif // ! FIREWORKS_H_

5.2 fireworks.cpp

  下面是 fireworks.cpp 文件的内容:

#include <cmath>
#define SHOW_CONSOLE
#include "fireworks.h"

const double Fireworks::particleSpeed = 3.0f;

Fireworks::Fireworks()
{
	init();
}

void Fireworks::init()
{
	delayTime = rand() % 300 + 20;
	riseTime = rand() % 80 + 160;
	bloomTime = 160;

	risePos.x = rand() % 450 + 300.0f;
	risePos.y = GROUND;

	riseSpeed.y = myrand(1.0f) - 3.0f;	//上升速度,根据坐标系需要是负的
	riseSpeed.x = myrand(0.4f) - 0.2f;	//可稍微倾斜

	//随机颜色
	color = HSVtoRGB(myrand(360.0f), 1.0f, 1.0f);

	//给每一个粒子设置初始速度
	for (int i = 0; i < NUM_PARTICLE - 1; i += 2)
	{
		//为了球状散开,设初始速度大小相等
		//初始随机速度水平角度和垂直角度,因为看到是平面的,所以求x, y分速度
		double levelAngle = randomf() * 360;
		double verticalAngle = randomf() * 360;

		//速度投影到xOy平面
		double xySpeed = particleSpeed * cos(verticalAngle);

		//求x, y分速度
		p[i].speed.x = xySpeed * cos(levelAngle);
		p[i].speed.y = xySpeed * sin(levelAngle);

		//动量守恒,每对速度反向
		if (i + 1 < NUM_PARTICLE) {
			p[i + 1].speed.x = -p[i].speed.x;
			p[i + 1].speed.y = -p[i].speed.y;
		}
	}
}

void Fireworks::draw(PIMAGE pimg)
{
	//未开始
	if (delayTime > 0)
		return;
	//烟花上升阶段
	else if (riseTime > 0) {
		setfillcolor(color, pimg);
		//画四个点,这样大一些
		bar(risePos.x, risePos.y, risePos.x + 2, risePos.y + 2, pimg);
	}
	//烟花绽放阶段
	else {
		setfillcolor(color, pimg);
		for (int i = 0; i < NUM_PARTICLE; i++) {
			bar(p[i].pos.x, p[i].pos.y, p[i].pos.x + 2, p[i].pos.y + 2, pimg);
		}
	}
}

//更新位置等相关属性
void Fireworks::update()
{
	if (delayTime-- > 0)
		return;
	//处于上升阶段,只更新烟花位置
	else if (riseTime > 0) {
		risePos.x += riseSpeed.x;
		risePos.y += riseSpeed.y;

		//重力作用
		riseSpeed.y += 0.005;

		//上升完毕,到达爆炸阶段
		if (--riseTime <= 0) {
			//设粒子初始位置为烟花当前位置
			for (int i = 0; i < NUM_PARTICLE; i++) {
				p[i].pos.x = risePos.x;
				p[i].pos.y = risePos.y;
			}
		}
	}
	//烟花绽放阶段
	else if (bloomTime-- > 0) {
		//粒子散开,更新粒子位置
		for (int i = 0; i < NUM_PARTICLE; i++) {
			p[i].pos.x += p[i].speed.x;
			p[i].pos.y += p[i].speed.y;

			//重力作用
			p[i].speed.y += 0.005;

			//速度减慢
			p[i].speed.x *= 0.982;
			p[i].speed.y *= 0.982;
		}
	}
	else {
		//烟花重新开始
		init();
	}
}
5.3 main.cpp

  下面是 main.cpp 文件的内容:

  用到了两个文件,一张背景图片和一首背景音乐,要放在main.cpp文件旁边。代码中用的是相对路径,直接默认文件位于当前目录下。


#include <time.h>
#include <graphics.h>

#include "fireworks.h"

#define NUM_FIREWORKS 12	//烟花数量,12左右比较好,多了太密集

int main()
{
	initgraph(800, 800, INIT_RENDERMANUAL);

	srand((unsigned)time(NULL));

	//烟花
	Fireworks* fireworks = new Fireworks[NUM_FIREWORKS];

	//背景图(800 x 800)
	PIMAGE bgPimg = newimage();
	getimage(bgPimg, "花火bg.jpg");
	//先绘制一下,不然前面有空白期
	putimage(0, 0, bgPimg);
	delay_ms(0);

	//背景音乐
	MUSIC bgMusic;
	bgMusic.OpenFile("羽肿 - 花火が瞬く夜に.mp3");
	bgMusic.SetVolume(1.0f);
	if (bgMusic.IsOpen()) {
		bgMusic.Play(0);
	}

	
	//图像缓存, 因为要加背景图,直接加模糊滤镜会把背景图模糊掉
	//所以另设一个图像缓存来绘制烟花并加模糊滤镜,再绘制到窗口
	PIMAGE cachePimg = newimage(800, 800);
	
	//计时用,主要用来定时检查音乐播放
	int timeCount = 0;

	for (; is_run(); delay_fps(60))
	{
		//隔1秒检查一下,如果播放完了,重新播放
		if ((++timeCount % 60 == 0) && (bgMusic.GetPlayStatus() == MUSIC_MODE_STOP)) {
			bgMusic.Play(0);
		}
		//更新位置
		for (int i = 0; i < NUM_FIREWORKS; i++) {
			fireworks[i].update();
		}
		
		//清屏
		cleardevice();	
		//绘制背景
		putimage(0, 0, bgPimg);

		//绘制烟花到图像缓存中
		for (int i = 0; i < NUM_FIREWORKS; i++) {
			fireworks[i].draw(cachePimg);
		}

		//模糊滤镜,拖尾效果
		//第二个参数,模糊度,越大越模糊,粒子也就越粗
		//第三个参数,亮度,越大拖尾越长
		//可以试试一下其它参数搭配,例如以下几组:
		//0x03, 0xff
		//0x0b, 0xe0
		//0xff, 0xff
		imagefilter_blurring(cachePimg, 0x0a, 0xff);

		//缓存绘制到窗口,模式为(最终颜色 = 窗口像素颜色 Or 图像像素颜色), 这样颜色会叠加起来
		putimage(0, 0, cachePimg, SRCPAINT);

	}

	//释放动态分配的内存
	delete[] fireworks;

	//删除图像,释放资源
	delimage(bgPimg);
	delimage(cachePimg);

	//释放音频资源
	bgMusic.Close();

	closegraph();

	return 0;
}

6. 运行过程截图

  背景图原来是有烟花的,PS抠掉了,不会抠图,有些没抠掉。本来想把灯笼单独抠出来做张图像,也不会抠。背景图里有水,水里少了倒影,这个是可以计算出倒影的位置的,粒子根据地面位置对称一下就完事了。但是水边有草,这个应该在上面,应该要单独抠出来,最后放上去,不然一弄下来草上是有烟花的。不会抠,懒得弄。
  (背景图和背景歌曲都来自羽肿的轻音乐 《花火が瞬く夜に》, 挺喜欢的 ٩(๑>◡<๑)۶ )

在这里插入图片描述


EGE专栏:EGE专栏

  • 30
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
EGE图形库是一款简单易用的图形库,它支持绘制基本的图形、文字,还有一些常用的控件,如按钮、文本框、滚动条等。下面是一个简单的EGE图形库滚动条控件示例代码: ``` #include <graphics.h> #include <conio.h> int main() { initgraph(640, 480); // 初始化图形界面 int x = 300; // 滚动条位置 int barWidth = 20; // 滚动条宽度 int barHeight = 100; // 滚动条高度 int scrollHeight = 300; // 滚动区域高度 int barTop = (480 - barHeight) / 2; // 滚动条顶部位置 int scrollTop = (480 - scrollHeight) / 2; // 滚动区域顶部位置 while (true) { cleardevice(); // 清空屏幕 // 绘制滚动区域 setfillcolor(LIGHTGRAY); bar(100, scrollTop, 540, scrollTop + scrollHeight); // 绘制滚动条背景 setfillcolor(WHITE); bar(x, barTop, x + barWidth, barTop + barHeight); // 绘制滚动条 setfillcolor(DARKGRAY); bar(x, barTop + (scrollHeight - barHeight) * (x - 100) / (540 - 100 - barWidth), x + barWidth, barTop + (scrollHeight - barHeight) * (x - 100) / (540 - 100 - barWidth) + barHeight); if (kbhit()) // 监听键盘事件 { char ch = getch(); if (ch == 'w' || ch == 'W' || ch == VK_UP) // 向上移动滚动条 { if (x > 100) { x--; } } else if (ch == 's' || ch == 'S' || ch == VK_DOWN) // 向下移动滚动条 { if (x < 540 - barWidth) { x++; } } } delay(10); // 延迟10毫秒,控制帧率 flushmouse(); // 刷新鼠标状态 } closegraph(); // 关闭图形界面 return 0; } ``` 这段代码实现了一个简单的滚动条控件,可以通过键盘上下箭头或W、S键来控制滚动条的位置。滚动条会根据位置的改变,自动调整滚动区域的显示内容。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依稀_yixy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值