• 《C++游戏开发》笔记十一 平滑动画:不再颤抖的小雪花


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

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

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





          在上一节雾央讲解了一下平面的障碍物判定,本来打算讲解一下斜坡的障碍物判定,但是有朋友推荐了一片文章,对障碍物判定讲解的非常好,雾央就直接把地址贴出来,就不重复了。

                     2D游戏中的障碍判定

          这篇文章讲解了2D游戏的很多东西,大家可以好好看看,雾央也很希望研究游戏开发的朋友们可以推荐下好文章,一起进步,在此谢过。

          另外,从本节笔记开始,雾央决定把系列名称改为《C++编程》,具体原因请移步笔记一

         

          在之前雾央讲解了背景滚动,不知道大家发现了没有,在人物移动的时候,画面是一卡一卡的,原因是由于大家按下方向键移动后,人物突然移动一段距离,而背景也跟随着移动一段距离,突然变化的画面给大家带来的就是一种卡的感觉。在之后的粒子系统中有同学提到颤抖的小雪花同样是这样,在每次绘制的时候,都让雪花增加了一段确定的位移,但是由于计算机状态的不断变化,每两帧之间的时间差不同,所以雪花的移动也就不规律。有颤抖的感觉。


          由上面的讨论,大家应该能知道,在游戏中,通过增加确定的位移来改变某个事物的位置往往是不准确的,也会导致画面不流畅。但是思考这种抖动产生的原因,我们可以受到很多启发,我们是不是可以减少每次移动的距离,而增加移动的频率来达到同样的位置呢?这样就可以大大降低画面的违和感,而最高的频率也就是和画面绘制的频率一致了,因此我们可以在每次绘制画面的时候进行较小幅度的更新,而为了摆脱两次绘制之间的时间差的波动,我们需要计算时间差,更新幅度和时间差关联起来。

         

          最直接的实现方式当然就是通过时间和速度来达到上述效果了。

          比如对于人物,我们给予他x和y方向上的速度,通过时间乘以速度来更新他的位置。玩家的操作会改变人物的方向和速度。粒子系统亦然。

     

          大家知道了原理之后,在实现之前,我们首先要解决几个问题

          首先是获取两帧之间的时间差。这个要求我们记录下上次绘制的时间,在每次绘制时候获取当前时间,两者之间差值即为所求。大家可以使用全局变量来记录上次绘制的时间,在这里雾央使用的是静态变量。

     

    static float lastTime=timeGetTime();   
    static float currentTime=timeGetTime();
    currentTime=timeGetTime();
    //状态更新
    lastTime=currentTime;


     

          这样之后即可以得到平滑的动画了,雾央仍然是以之前的粒子系统中的雪花为例,这样大家就可以和以前的方式进行一下对比,就可以更清楚的看出差别了。

          另外,像这样获取帧之间的时间差后,大家就可以计算出FPS了,帧数的计算很简单,就是画面绘制的次数除以绘制花费的时间即可,为了不让帧数不断的跳动,大家可以隔一段时间比如1秒计算一次。

    //获取当前FPS
    float CalculateFPS()
    {
        static float  fps = 0;           //FPS值   
        static int    frameCount = 0;    //帧数   
        static float  currentTime =0.0f;   
        static float  lastTime = 0.0f;  
        //每绘制一次,帧数加1
        frameCount++;           
        currentTime = timeGetTime()/1000.0f;
     
    //大家是可以每绘制一次就计算一次帧数,即1/时间即可,这样得到的是实时帧数,
    //但是由于时间间隔太短,帧数变化太快,显示在窗口上也看不清,所以每隔一段时间计算一次 
        if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟   
        {  
            fps = frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值   
            lastTime = currentTime; 
            frameCount    = 0;        
        }  
      
        return fps;  
    }

          

          在之前的笔记中,涉及到动画都使用了定时器,雾央在初学游戏的时候,代码里是一大堆的定时器,呵呵。但是现在我们就可以不再使用这么多的定时器,如果大家直接使用WIN32,可以完全不使用定时器了,但是用MFC的话,大家需要使用一个定时器驱动OnPaint绘图,剩余的动画大家就可以利用计算时间差来更新了

          比如每100毫秒更新一帧的动画

    if(currentTime-lastTime>100.0f)
         更新下一帧

          另外大家需要注意一点,贴图的时候是按像素定的位置,也就是位置必须是整型的数值,但是由于采用时间*速度来计算位置,每次加上的部分很小,所以xy坐标都要是浮点型的,但是绘制的时候是取了整的。 

     

          和上一次讲粒子系统不同的是,这一次雾央封装了一个粒子类,代码看上去比以前整洁多了,另外雾央这一次把宏定义放到stdafx.h中去了,因为多个文件都需要使用到一些宏定义。

     

          雾央先定义了一个粒子的结构体

     

    struct snow
    {
    	float x;
    	float y;
    	float speed; //速度
    	int number;  //粒子图像编号
    };

    下面是粒子类

     

    class CParticle
    {
    private:
    	int m_number; //数量
    	struct snow *m_pSnow;  //雪花
    	CImage m_snowMap[7]; //七种雪花图像
    public:
    	CParticle(int number);
    	~CParticle();
    
    public:
    	void Init();  //初始化粒子
    	void Draw(CDC &cDC);  //绘制粒子
    	void Update(float time);//更新粒子
    };
    

    粒子类的具体实现

     

    #include"stdafx.h"
    #include"particle.h"
    
    CParticle::CParticle(int number)
    {
    	m_number=number;
    	m_pSnow=new struct snow[m_number];
    }
    
    void CParticle::Init()
    {
    	//加载雪花图像
    	char buf[20];
    	for(int i=0;i<7;i++)    //加载七种图像
    	{
    		sprintf(buf,"Snow//%d.png",i);
    		m_snowMap[i].Load(buf);
    		TransparentPNG(&m_snowMap[i]);
    	}
    	//初始化雪花粒子
    	for(int i=0;i<m_number;i++)
    	{
    		m_pSnow[i].x=rand()% WINDOW_WIDTH;   //最初雪花在水平方向上随机出现
    		m_pSnow[i].y=rand()% WINDOW_HEIGHT; //垂直方向上也是随机出现
    		m_pSnow[i].number=rand()%7;         //七种雪花中的一种
    		m_pSnow[i].speed=(rand()%5+1)/20.0;
    	}
    }
    
    void CParticle::Draw(CDC &cDC)
    {
    	//绘制雪花粒子
    	for(int i=0;i<m_number;i++)
    		m_snowMap[m_pSnow[i].number].Draw(cDC,m_pSnow[i].x,m_pSnow[i].y,32,32);
    }
    
    void CParticle::Update(float time)
    {
    	for(int i=0;i<m_number;i++)
    	{
    		m_pSnow[i].y+=time*m_pSnow[i].speed;
    		if(m_pSnow[i].y>WINDOW_HEIGHT)
    			m_pSnow[i].y=-32;  
    	}
    }
    
    CParticle::~CParticle()
    {
    	delete[] m_pSnow;
    }
    

    最后是CChildView头文件

     

    // ChildView.h : CChildView 类的接口
    //
    
    #pragma once
    #include "particle.h"
    
    // CChildView 窗口
    
    class CChildView : public CWnd
    {
    // 构造
    public:
    	CChildView();
    
    // 特性
    public:
    
    	CRect m_client;    //保存客户区大小
    	CImage m_bg;      //背景图片
    
    	CParticle *m_snow;
    	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:
    	
    	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"
    
    #include "mmsystem.h"
    #pragma comment(lib,"winmm.lib")//导入声音头文件库
    
    #ifdef _DEBUG
    #define new DEBUG_NEW
    #endif
    
    
    // CChildView
    
    CChildView::CChildView()
    {
    }
    
    CChildView::~CChildView()
    {
    	mciSendString("stop bgMusic ",NULL,0,NULL);
    	delete m_snow;
    }
    
    
    BEGIN_MESSAGE_MAP(CChildView, CWnd)
    	ON_WM_PAINT()
    	ON_WM_TIMER()
    	ON_WM_CREATE()
    END_MESSAGE_MAP()
    
    
    
    // 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("bg.png");
    
    	//打开音乐文件
    	mciSendString("open background.mp3 alias bgMusic ", NULL, 0, NULL);
    	mciSendString("play bgMusic repeat", NULL, 0, NULL);
    
    	m_snow=new CParticle(100);
    	//初始化
    	m_snow->Init();
    
    	return TRUE;
    }
    
    void CChildView::OnPaint() 
    {
    	static float lastTime=timeGetTime();    
    	static float currentTime=timeGetTime();
    	//获取窗口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);
    	//————————————————————开始绘制——————————————————————
    	//贴背景,现在贴图就是贴在缓冲DC:m_cache中了
    	m_bg.Draw(m_cacheDC,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,0,0,WINDOW_WIDTH,WINDOW_HEIGHT);
    	//贴雪花
    	m_snow->Draw(m_cacheDC);
    	//更新雪花
    	currentTime=timeGetTime();
    	m_snow->Update(currentTime-lastTime);
    	lastTime=currentTime;
    	//最后将缓冲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::OnTimer(UINT_PTR nIDEvent)
    {
    	
    	OnPaint();
    }
    
    
    int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
    	if (CWnd::OnCreate(lpCreateStruct) == -1)
    		return -1;
    
    	// TODO:  在此添加您专用的创建代码
    
    	//创建一个10毫秒产生一次消息的定时器
    	SetTimer(TIMER_PAINT,10,NULL);
    
    	return 0;
    }
    


    来看几张运行图片,大家可能从画面上看不出和上次的区别,但是实际运行一下,大家就知道要比上次的强几百倍了

     
    另外,这次每个雪花的速度雾央也设置成一定范围内随机的,看起来更带感了


     具体的就留给大家下载demo回去自己体会了

    PS:背景音乐是雅尼的夜莺,很好听的曲子,希望大家能喜欢。


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


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

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



  • 相关阅读:
    Asp.net与SQL一起打包部署安装(收集)
    算法:计算1的个数
    安装oracle817后出现的问题
    DataTime.ToString 的方法。
    历史上最经典智力题
    困惑,个人资源管理混乱不堪。
    Test for this blog
    转:一个硕士应届生的面试总结.(很精彩,特别是具体的各公司面试信息)
    太强了,不转不行,学习JAVA必备!(转载)
    可定制个性化页面JS(转)
  • 原文地址:https://www.cnblogs.com/java20130723/p/3211342.html
Copyright © 2020-2023  润新知