• 《MFC游戏开发》笔记八 游戏特效的实现(二):粒子系统


    本系列文章由七十一雾央编写,转载请注明出处。

    http://blog.csdn.net/u011371356/article/details/9360993

    作者:七十一雾央 新浪微博:http://weibo.com/1689160943/profile?rightmod=1&wvr=5&mod=personinfo


     

           在游戏之中,大家经常看到火焰、爆炸、烟、水流、火花、落叶、云、雾、雪、尘、流星尾迹或者像发光轨迹这样的抽象视觉效果等等,这些效果看起来都非常绚丽,为游戏增添了不少美感,可以说凡是出色游戏都必不可少,通过学习今天的笔记,大家以后就可以在自己的游戏中加入这些效果了,呵呵。 

          

           大家学到这里已经知道游戏中那些华丽的效果都是通过贴图实现的,但是显然这些效果不是简单的贴一张或几张图就可以做到的,毕竟它是动态的。在游戏开发中,这种效果一般被称为粒子系统,所谓系统,就是一堆粒子的集合体。因此大家看到的火焰,其实就是很多个小火花聚集在一起显示出来的效果。、

          

           在今天的笔记中,雾央将带着大家一步一步实现雪花漫天飞舞的场景。

           惯例,先来几张效果图,激发一下大家学习的兴趣,呵呵

           PS:由于雾央给力的美工同学不在身边,所以雪花的图片是自己抠的,看起来边缘处有点惨不忍睹,大家原谅一下几乎不会PS的雾央吧。

           

          在满天飞雪中行走的感觉真好,呵呵

            


    一、粒子信息的记录


           在雾央的这个程序中,一屏幕中有100个雪花,每个雪花都是一个粒子,都需要我们单独的绘制出来。它们有位置,有雪花样式(PS:总共有七种雪花哦),这些信息都需要我们记录下来。用一个粒子类是比较合适的,但是这里我们先用结构体吧。

           首先定义一下粒子结构体

     

    //粒子结构体
    struct snow
    {
            int x;     //粒子的x坐标
            int y;	   //粒子的y坐标
            int number;//粒子编号,共七种粒子
    }Snow[SNOW_NUMBER];    

           雾央在这里定义了一个宏来表示雪花的数量

    #define SNOW_NUMBER 100  //雪花例子的数量

     

    大家可以修改数值,就可以让雪花从小雪变到暴雪了,不过如果数量太多的话,会导致帧数严重下降,造成游戏很卡,大家根据自己的显卡适可而止,呵呵。


    二、粒子信息初始化


           为了营造雪花随机出现的情景,我们在初始化时就设置粒子的位置是随机的。使用rand函数就可以产生一个随机数啦,注意我们的粒子出现的范围要在窗口之内(严格来说,游戏之中粒子出现的范围应该是大地图范围,这里为了简化,所以雾央设置他们出现在窗口范围,即和地图脱节了,否则还需要进行粒子的可见性判断,这些雾央会放在以后进行,现在先慢慢来)。

     

    //初始化雪花粒子
    for(int i=0;i<SNOW_NUMBER;i++)
    {
    	Snow[i].x=rand()% WINDOW_WIDTH;   //最初雪花在水平方向上随机出现
    	Snow[i].y=rand()% WINDOW_HEIGHT; //垂直方向上也是随机出现
    	Snow[i].number=rand()%7;         //七种雪花中的一种
    }

    三、粒子的绘制


           我们有100个粒子,依次绘制出来就可以。这里唯一需要注意的是绘制各种景物的顺序,我们希望看到的是雪花打在人物身上,因此贴图的次序是先绘制背景,接着绘制人物,最后绘制雪花,如果弄反了,就会出现雪花在人物身后很远的地方飘舞的效果,哈哈。

     

    四、粒子的更新


            粒子如果是一动不动的,那真是见鬼了。首先至少要有的是雪花下落吧。这个简单,将粒子的y坐标增加就可以了,如果我们还想营造出有风的感觉,即水平方向上雪花也在飘动,那么让粒子的x坐标也改变就可以了。这里需要注意的是当雪花飘出窗口范围的时候,我们得让它回到窗口,否则雪花就越来越少,最后就木有了,成为瞬时雪了。

     

    //绘制雪花粒子
    for(int i=0;i<SNOW_NUMBER;i++)
    {
    	//画出粒子
    	m_snowMap[Snow[i].number].Draw(m_cacheDC,Snow[i].x,Snow[i].y,32,32);
    	//对粒子的位置进行更新
    	Snow[i].y+=1;
    	if(Snow[i].y>=600)    //当落到最下面后,再回到最上面去
    		Snow[i].y=0;
    	//为了更自然,在水平方向上也发生位移,就像有风一样
    	if(rand()%2==0)
    		Snow[i].x+=1;
    	else 
    		Snow[i].x-=1;
    	if(Snow[i].x<0)
    		Snow[i].x=WINDOW_WIDTH;      //水平方向上出界后到另一边去
    	else if(Snow[i].x>=WINDOW_WIDTH)
    		Snow[i].x=0;
    }

           大家玩游戏看到的绚丽的效果,很多都是由粒子系统实现的。而粒子系统的实现也就这么四个步骤:定义,初始化,绘制,更新,在更新的时候有的还涉及到消亡的问题。所以只要大家理解了,实现起来应该是不难的。


           在上完整的源代码之前,雾央有几点想说的。

           这个粒子系统实现的比较简单,大家学习了这节笔记后,可以自己改进,让它更完善,比如让雪花飘落的时候旋转,这个是不是就更有感觉了呢,哈哈。或者大家可以尝试定个时,比如每隔两分钟就下一次雪等等,我相信这样,大家会进步的更快。

     

           另外这个粒子系统的实现和之前的背景滚动一样实现的并不好,不知道大家注意到了没有,画面是一卡一卡的,简直是一场悲剧。这个请大家先想一下怎么解决,如果不出意外的话,雾央将在下节笔记中进行讲解流畅动画的实现。

          雾央准备从下节笔记开始使用类进行封装,使代码不至于像现在这样凌乱,不知道大家都有没有C++面向对象的基础,请大家积极留言,让雾央知道是不是有必要简单的讲解一下面向对象的基本知识。

     

         还有,雾央很少看到大家的评论,也不知道自己讲解的有哪些不足。请大家积极留言,发表一下自己的看法,可以说下哪里讲的不好,也可以谈下希望讲解的内容,也可以建议一下讲解方式,雾央希望看到大家的看法,这也是支持雾央继续写下去的动力。

     

     五、源代码欣赏


    头文件

    // ChildView.h : CChildView 类的接口
    //
    
    
    #pragma once
    
    #define SNOW_NUMBER 100  //雪花例子的数量
    // CChildView 窗口
    
    class CChildView : public CWnd
    {
    // 构造
    public:
    	CChildView();
    
    // 特性
    public:
    	//粒子结构体
    	struct snow
    	{
    		int x;     //粒子的x坐标
    		int y;	   //粒子的y坐标
    		int number;//粒子编号,共七种粒子
    	}Snow[SNOW_NUMBER];    
    	//雪花图像
    	CImage m_snowMap[7];
    	//英雄结构体
    	struct shero
    	{
    		CImage hero;     //保存英雄的图像
    		int x;             //保存英雄的位置
    		int y;
    		int direct;        //英雄的方向
    		int frame;         //运动到第几张图片
    	}MyHero;
    
    	CRect m_client;    //保存客户区大小
    	CImage m_bg;      //背景图片
    
    	int m_xMapStart;     //x方向上地图的起始点
    	int m_mapWidth;      //背景地图的宽度
    
    	CDC m_cacheDC;   //缓冲DC
    	CBitmap m_cacheCBitmap;//缓冲位图
    // 操作
    public:
    
    // 重写
    	protected:
    	virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
    
    // 实现
    public:
    	virtual ~CChildView();
    
    	// 生成的消息映射函数
    protected:
    	afx_msg void OnPaint();
    	DECLARE_MESSAGE_MAP()
    public:
    	void GetMapStartX();
    	afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
    	afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    	afx_msg void OnTimer(UINT_PTR nIDEvent);
    	afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    };
    


    CPP文件

    //-----------------------------------【程序说明】----------------------------------------------
    // 【MFC游戏开发】笔记八 粒子系统 配套源代码
    // VS2010环境
    // 更多内容请访问雾央CSDN博客 http://blog.csdn.net/u011371356/article/category/1497651
    // 雾央的新浪微博: @七十一雾央
    //------------------------------------------------------------------------------------------------
    
    
    // ChildView.cpp : CChildView 类的实现
    //
    
    #include "stdafx.h"
    #include "GameMFC.h"
    #include "ChildView.h"
    
    #ifdef _DEBUG
    #define new DEBUG_NEW
    #endif
    
    //定时器的名称用宏比较清楚
    #define TIMER_PAINT 1
    #define TIMER_HEROMOVE 2
    //四个方向
    #define DOWN 0
    #define LEFT 1
    #define RIGHT 2
    #define UP 3
    //窗口大小
    #define WINDOW_WIDTH 800
    #define WINDOW_HEIGHT 600
    // CChildView
    
    CChildView::CChildView()
    {
    }
    
    CChildView::~CChildView()
    {
    }
    
    
    BEGIN_MESSAGE_MAP(CChildView, CWnd)
    	ON_WM_PAINT()
    	ON_WM_KEYDOWN()
    	ON_WM_LBUTTONDOWN()
    	ON_WM_TIMER()
    	ON_WM_CREATE()
    END_MESSAGE_MAP()
    
    
    //将png贴图透明
    void TransparentPNG(CImage *png)
    {
    	for(int i = 0; i <png->GetWidth(); i++)
    	{
    		for(int j = 0; j <png->GetHeight(); j++)
    		{
    			unsigned char* pucColor = reinterpret_cast<unsigned char *>(png->GetPixelAddress(i , j));
    			pucColor[0] = pucColor[0] * pucColor[3] / 255;
    			pucColor[1] = pucColor[1] * pucColor[3] / 255;
    			pucColor[2] = pucColor[2] * pucColor[3] / 255;
    		}
    	}
    }
    
    // CChildView 消息处理程序
    
    BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) 
    {
    	if (!CWnd::PreCreateWindow(cs))
    		return FALSE;
    
    	cs.dwExStyle |= WS_EX_CLIENTEDGE;
    	cs.style &= ~WS_BORDER;
    	cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, 
    		::LoadCursor(NULL, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), NULL);
    	
    	//-----------------------------------游戏数据初始化部分-------------------------
    	
    	//加载背景
    	m_bg.Load("bigbg.png");
    	//获取背景地图的宽度
    	m_mapWidth=m_bg.GetWidth();
    	//加载英雄图片
    	MyHero.hero.Load("heroMove.png");
    	TransparentPNG(&MyHero.hero);
    	//初始化英雄状态
    	MyHero.direct=UP;
    	MyHero.frame=0;
    	//设置英雄初始位置
    	MyHero.x=80;    
    	MyHero.y=400;
    	//设置地图初始从最左端开始显示
    	m_xMapStart=0;
    	//加载雪花图像
    	char buf[20];
    	for(int i=0;i<7;i++)    //加载七种图像
    	{
    		sprintf(buf,"Snow//%d.png",i);
    		m_snowMap[i].Load(buf);
    	}
    	//初始化雪花粒子
    	for(int i=0;i<SNOW_NUMBER;i++)
    	{
    		Snow[i].x=rand()% WINDOW_WIDTH;   //最初雪花在水平方向上随机出现
    		Snow[i].y=rand()% WINDOW_HEIGHT; //垂直方向上也是随机出现
    		Snow[i].number=rand()%7;         //七种雪花中的一种
    	}
    	
    	return TRUE;
    }
    //计算地图左端x开始位置
    void CChildView::GetMapStartX()
    {
    	//如果人物不在最左边和最右边半个屏幕内时,地图的起始坐标是需要根据人物位置计算的。
    	if(MyHero.x<m_mapWidth-WINDOW_WIDTH/2 && MyHero.x>WINDOW_WIDTH/2)
    		m_xMapStart=MyHero.x-WINDOW_WIDTH/2;
    }
    //获取人物在屏幕上的坐标
    int GetScreenX(int xHero,int mapWidth)
    {
    	//如果人物在最左边和最右边半个屏幕内时,那么人物就处在屏幕中间
    	if(xHero<mapWidth-WINDOW_WIDTH/2 && xHero>WINDOW_WIDTH/2)
    		return WINDOW_WIDTH/2;
    	else if(xHero<=WINDOW_WIDTH/2)     //在最左边半个屏幕时,人物在屏幕上的位置就是自己的x坐标了
    		return xHero;
    	else 
    		return WINDOW_WIDTH-(mapWidth-xHero);  //在最右边半个屏幕
    }
    void CChildView::OnPaint() 
    {
    	//获取窗口DC指针
    	CDC *cDC=this->GetDC();
    	//获取窗口大小
    	GetClientRect(&m_client);
    	//创建缓冲DC
    	m_cacheDC.CreateCompatibleDC(NULL);
    	m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());
    	m_cacheDC.SelectObject(&m_cacheCBitmap);
    	//计算背景地图起始位置
    	GetMapStartX();
    	//————————————————————开始绘制——————————————————————
    	//贴背景,现在贴图就是贴在缓冲DC:m_cache中了
    	m_bg.Draw(m_cacheDC,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,m_xMapStart,0,WINDOW_WIDTH,WINDOW_HEIGHT);
    	//贴英雄
    	MyHero.hero.Draw(m_cacheDC,GetScreenX(MyHero.x,m_mapWidth),MyHero.y,80,80,MyHero.frame*80,MyHero.direct*80,80,80);
    	//绘制雪花粒子
    	for(int i=0;i<SNOW_NUMBER;i++)
    	{
    		//画出粒子
    		m_snowMap[Snow[i].number].Draw(m_cacheDC,Snow[i].x,Snow[i].y,32,32);
    		//对粒子的位置进行更新
    		Snow[i].y+=1;
    		if(Snow[i].y>=600)    //当落到最下面后,再回到最上面去
    			Snow[i].y=0;
    		//为了更自然,在水平方向上也发生位移,就像有风一样
    		if(rand()%2==0)
    			Snow[i].x+=1;
    		else 
    			Snow[i].x-=1;
    		if(Snow[i].x<0)
    			Snow[i].x=WINDOW_WIDTH;      //水平方向上出界后到另一边去
    		else if(Snow[i].x>=WINDOW_WIDTH)
    			Snow[i].x=0;
    	}
    	//最后将缓冲DC内容输出到窗口DC中
    	cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);
    
    	//————————————————————绘制结束—————————————————————
    	
    	//在绘制完图后,使窗口区有效
    	ValidateRect(&m_client);
    	//释放缓冲DC
    	m_cacheDC.DeleteDC();
    	//释放对象
    	m_cacheCBitmap.DeleteObject();
    	//释放窗口DC
    	ReleaseDC(cDC);
    }
    
    //按键响应函数
    void CChildView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
    {
    	//nChar表示按下的键值
    	switch(nChar)
    	{
    	case 'd':         //游戏中按下的键当然应该不区分大小写了
    	case 'D':
    		MyHero.direct=RIGHT;
    		MyHero.x+=5;
    		break;
    	case 'a':
    	case 'A':
    		MyHero.direct=LEFT;
    		MyHero.x-=5;
    		break;
    	case 'w':
    	case 'W':
    		MyHero.direct=UP;
    		MyHero.y-=5;
    		break;
    	case 's':
    	case 'S':
    		MyHero.direct=DOWN;
    		MyHero.y+=5;
    		break;
    	}
    }
    
    //鼠标左键单击响应函数
    void CChildView::OnLButtonDown(UINT nFlags, CPoint point)
    {
    	char bufPos[50];
    	sprintf(bufPos,"你单击了点X:%d,Y:%d",point.x,point.y);
    	AfxMessageBox(bufPos);
    }
    
    //定时器响应函数
    void CChildView::OnTimer(UINT_PTR nIDEvent)
    {
    	
    	switch(nIDEvent)
    	{
    	case TIMER_PAINT:OnPaint();break;  //若是重绘定时器,就执行OnPaint函数
    	case TIMER_HEROMOVE:               //控制人物移动的定时器
    		{
    			MyHero.frame++;              //每次到了间隔时间就将图片换为下一帧
    			if(MyHero.frame==4)          //到最后了再重头开始
    				MyHero.frame=0;
    		}
    		break;
    	}
    }
    
    
    int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
    	if (CWnd::OnCreate(lpCreateStruct) == -1)
    		return -1;
    
    	// TODO:  在此添加您专用的创建代码
    
    	//创建一个10毫秒产生一次消息的定时器
    	SetTimer(TIMER_PAINT,10,NULL);
    	//创建人物行走动画定时器
    	SetTimer(TIMER_HEROMOVE,100,NULL);
    	return 0;
    }
    




    本节笔记源代码请点这里下载    

     

          《MFC游戏开发》笔记八到这里就结束了,更多精彩请关注下一篇。如果您觉得文章对您有帮助的话,请留下您的评论,点个赞,能看到你们的留言是我最高兴的事情,因为这让我知道我正在帮助曾和我一样迷茫的少年,你们的支持就是我继续写下去的动力,愿我们一起学习,共同努力,复兴国产游戏。

            对于文章的疏漏或错误,欢迎大家的指出。


  • 相关阅读:
    Web前端开发——HTML概述
    Web前端开发——概述
    [nowCoder] 两个不等长数组求第K大数
    [nowCoder] 两个长度相同有序数组的中位数
    [nowCoder] 完全二叉树结点数
    [nowCoder] 子数组最大乘积
    [nowCoder] 局部最小值位置
    [LeetCode] Binary Tree Right Side View
    [C/CPP系列知识] Type difference of character literals 和 bool in C and C++
    [C/CPP系列知识] C++中extern “C” name mangling -- Name Mangling and extern “C” in C++
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3199062.html
Copyright © 2020-2023  润新知