首页 > 编程语言 >Re0:从零开始的C++游戏开发【中】

Re0:从零开始的C++游戏开发【中】

时间:2024-06-02 21:03:17浏览次数:32  
标签:动画 int Re0 pos C++ 玩家 player 从零开始 我们

Re0:从零开始的C++游戏开发 (中)

这是蒟蒻观看B站upVoidmatrix的课程从零开始的提瓦特幸存者的个人笔记【自用】

前言:采用适用于小白的easyx图形库。

第三集 提瓦特の幸存者

3.1 程序动画实现及角色移动

在开始之前,我们应该认识到,尽管我们可以通过点线面绘制简单的画面,但是想要只用这种矢量绘图的方式完成游戏内全部素材是远远不够的。想要绘制一个简单的人物就要洋洋洒洒300+行代码,那更不用提什么画质精美的3A大作了。

所以,使用经过专业绘图软件(如:PS等)处理的位图素材是必不可少的。位图素材也就是我们常说的图片资源素材。

那么,我们如何在Easyx中加载并渲染图片资源呢?我们查看文档就可发现,Easyx使用了一个叫做**IMAGE的类来表示图片对象**;而加载图片使用一个叫做**loadimage的函数**,这个函数负责将图片文件数据加载到IMAGE对象、或者直接将图片加载到绘图窗口中,同时这个函数还有一个重载,用以从资源文件中加载图像。

加载图片完成后,就是如何渲染图片,这里使用**putimage函数**。putimage函数同样有两个重载。

所以整套图片绘制的流程就是:

IMAGE img;
loadimage(&img,"test.jpg");
putimage(100,200,&img);

掌握这两个函数后,我们就可以开始编写代码了。

在一切开始之前按照先前所讲述的,将游戏框架写出来。

#include <graphics.h>

int main()
{
	initgraph(1280, 720);

	bool running = true;

	ExMessage msg;

	BeginBatchDraw();

	while (running)
	{
		DWORD start_time = GetTickCount();

		while (peekmessage(&msg))
		{

		}

		cleardevice();
		FlushBatchDraw();

		DWORD end_time = GetTickCount();
		DWORD delta_time = start_time - end_time;
		if (delta_time < 1000 / 144)
		{
			Sleep(1000 / 144 - delta_time);
		}
	}
	EndBatchDraw();

	return 0;
}

现在就可以将背景绘制在窗口中了:首先,将素材文件copy到工程目录下。需要注意的是,VS在调试时使用的相对路径、根目录和新建代码的默认位置相同。

在加载渲染好背景图片后,就到了我们的重点——如何让画面”动“起来?

游戏开发技术中,角色动画的常见实现可以笼统的分为两类序列帧动画和关键帧动画序列帧动画通常由一组图片素材组成,我们在程序中随着时间的推移不断切换显示这一序列的图片,借助视觉暂留效应,便有了动画效果;而关键帧动画骨骼动画等往往涉及到更复杂的图形学技术,在此暂不作讨论。

现在我们使用一组二次元人物图片作为游戏素材,要想实现每个一段时间切换一张图片显示,该如何处理呢?

3.1.1 动画实现

我们或许会想到Sleep()函数,例如:我们希望在一秒钟切换10次图片,那么只需要写下Sleep(100);这样的代码就可以了,吗?但是,我们在之前提及过,当调用Sleep()函数时,程序会卡在这里等待对应的时间,这是一个”阻塞式“的行为;而在我们的游戏框架设计中,所有的画面渲染等操作,都应该在一次又一次的循环中进行,每次循环的时间都应该控制在1/60秒内,也就是说,我们切换动画轮播的任务,应该分摊在多帧之间进行,而不是在单次循环内一次性解决。

这就触及到我们游戏编程的一个核心思想主循环内应尽量避免阻塞式的行为或过于繁重且耗时过长的任务。具体可以进入**“高性能”编程领域**深入学习。

为了确保动画序列帧的能够间隔固定的时间进行切换,我们这里类比定时器的概念实现一个计数器

首先,定义idx_cur_anim变量来存储当前动画的帧索引;再定义一个counter用来记录当前动画帧一共播放了几个游戏帧,这里使用staic修饰计数器,保证计数器只在第一个游戏帧时被初始化为0,我们不妨每5个游戏帧切换动画帧。

随后,我们还要考虑到动画帧序列播放结束后的行为,我们希望动画是循环播放的,也就是当动画的帧索引到大帧总数时,将索引重置为0。

const int PLAYER_ANIM_NUM = 4;
int main()
{
    /* ...
    ...*/
    static int counter = 0;
	if(++counter % 5 == 0)
    {
        idx_cur_anim ++;
    }
	idx_cur_anim =  idx_cur_anim % PLAYER_ANIM_NUM;
    /* ...
    ...*/
}
	

这样,我们就完成了动画的数据逻辑部分,接下来就是动画的渲染部分

在这之前,我们首先应该像加载背景图片那样将动画的每一帧图片都加载到程序中。定义LoadAnimation()函数。我们将图片规律命名,这样就可以使用循环加载图片。在使用**Unicode字符集**的情况下,我们可以使用wstring来拼凑出文件路径,进而传递给loadimage()函数,将图片加载到数组中。

现在来到游戏框架中的画面渲染部分,之前定义的动画帧索引这时便可以当作IMAGE数组的索引来使用。

但运行程序我们会发现,虽然人物动画轮播功能是正常的,但人物的周围套上了黑黑的边框。看起来图片的透明区域并未发生作用,这是因为putimage()函数在渲染过程中,并没有使用IMAGE对象的透明度信息,所以我们想要绘制类似这种带有透明度的图片素材,就要自己处理这部分逻辑。这里,我们类比putimage()函数封装一个putimage_alpha()函数。

// 实现透明通道混叠 借助系统绘图函数的比较轻巧的实现
#pragma comment(lib,"MSIMG32.LIB")

inline void putimage_alpha(int x, int y, IMAGE* img)
{
	int w = img->getwidth();
	int h = img->getheight();
	AlphaBlend(GetImageHDC(NULL), x, y, w, h,
		GetImageHDC(img), 0, 0, w, h, {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA});
}

再次运行程序,就可以发现动画被正常渲染了。

3.1.2 角色移动

接着,我们来实现键盘控制角色移动的功能。

我们首先定义POINT类型的player_pos变量用来存储玩家的位置,记得将玩家坐标初始化。随后将动画渲染的位置更改为player_pos变量的位置。

这时,只需要在事件处理部分根据按键修改player_pos的值,就可以实现角色的移动。

我们只需要对键盘按下的消息进行处理,定义PLAYER_SPEED常量表示玩家速度,并约定使用方向键控制玩家移动。

/*...
...*/
	while(peekmessage(&msg))
    {
        if(msg.message = WM_KEYDOWN)
        {
            switch(msg.vkcode)
            {
                case VK_UP:
                    player_pos.y -= PLAYER_SPEED;
                    break;
                case VK_DOWN:
                    player_pos.y += PLAYER_SPEED;
                    break;   
                case VK_LEFT:
                    player_pos.x -= PLAYER_SPEED;
                    break;
                 case VK_RIGHT:
                    player_pos.x += PLAYER_SPEED;
                    break;
            }
        }
    }
/*...
...*/

关于键码对照表可以查看微软官方文档。

运行程序,我们可以发现角色可以移动了,但人物的移动“手感”有些奇怪。当我们按下方向键,角色向着对应的方向抽搐了一下,一段时间后才进行较为连贯的移动,在连续移动的过程中顿挫感也十分明显。

出现此等原因主要有二1.首先是持续按下一小段时间后才开始连贯移动的问题。这是因为当我们按下方向键时,会首先有一个WM_KEYDOWN消息进入消息事件队列中,随后,当我们我们保持按键按下状态一段时间后,才会有接连不断的WM_KEYDOWN消息被触发;2.然后是移动过程中的卡顿问题。这是因为WM_KEYDOWN消息的产生是与我们的主循环异步进行的,且触发的频率与操作系统和硬件设备相关,这就导致在有些游戏帧中事件处理部分对多个WM_KEYDOWN消息进行了处理,而在其余游戏帧中WM_KEYDOWN消息较少或没有,这就导致角色在某些游戏帧中前进的距离较远/近一些,在宏观上展现为移动过程中的卡顿感。

解决问题就要理清思路,我们抽象地总结实际的功能需求:当按键按下时,我们要确保在每一个游戏帧中都连贯的移动相同的距离;从玩家的行为角度讲,也就是玩家按下按键时,WM_KEYDOWN消息触发,标志角色开始移动;而当玩家按键抬起时,WM_KEYUP消息触发,标志移动结束

那么我们的解决方案就明晰了。我们首先定义4个bool变量分别标志玩家是否向对应方向移动。在事件处理部分,不直接对玩家的位置数据进行操作,而是设置这些布尔变量的值,按键按下设为true、按键抬起设为false。在数据处理部分,我们再根据这些布尔变量的状态确定是否对玩家的位置进行处理。

/*...
...*/
	bool is_move_up = false;
	bool is_move_down = false;
	bool is_move_left = false;
	bool is_move_right = false;
/*...
...*/
	while(running)
	{
        /*...
		...*/
    	while(peekmessage(&msg))
    	{
        	if(msg.message = WM_KEYDOWN)
        	{
           		switch(msg.vkcode)
            	{
                case VK_UP:
                    is_move_up = true;
                    break;
                case VK_DOWN:
                    is_move_down = true;
                    break;   
                case VK_LEFT:
                    is_move_left = true;
                    break;
                 case VK_RIGHT:
                    is_move_right = true;
                    break;
                }
        	}
            else if(msg.message = WM_KEYUP)
            {
                switch(msg.vkcode)
            	{
                case VK_UP:
                    is_move_up = false;
                    break;
                case VK_DOWN:
                    is_move_down = false;
                    break;   
                case VK_LEFT:
                    is_move_left = false;
                    break;
                 case VK_RIGHT:
                    is_move_right = false;
                    break;
                }
			}
    	}
        if(is_move_up) plaayer_pos.y -= PLAYER_SPEED;
        if(is_move_down) plaayer_pos.y += PLAYER_SPEED;
        if(is_move_left) plaayer_pos.x -= PLAYER_SPEED;
        if(is_move_right) plaayer_pos.x += PLAYER_SPEED;
        /*...
		...*/
	}
/*...
...*/

3.2 敌人随机生成和索敌逻辑实现

3.2.1 动画类实现

到目前为止,我们已经实现了人物面向左的动画,那么面向右的动画同理:定义IMAGE数组,加载图片到IMAGE数组中,然后在主循环中使用计数器来更新动画的帧索引,最后在绘图阶段将对应帧索引的图片绘制出来。但是这样一来,我们就有两部分能极度相似动画播控代码了,若后续仍有动画加入到游戏中,我们就还要讲这些代码再写一遍,这就造成了代码冗余

我们所使用的不同动画之间的区别,无非只是加载和显示的图片不同,而其中更新帧索引和绘制的部分都是完全一样的代码。

于是,我们可以将动画封装成结构体或类,相同的逻辑封装成成员方法,不同的部分使用参数传递。没错,这就是面向对象的3大特性之一的封装

我们这里定义**Animation类**,用来封装动画相关的数据和逻辑。接下来,我们在填充类的细节的时候,要考虑的就是有哪些数据和功能放在类内部

**首先是动画的图片加载。**考虑到动画所包含的图片帧数量可能是不同的,需要动态的为图片对象序列分配内存,所以这里使用动态数组(向量)vector容器来代替我们常见的数组。

vector容器是STL(标准模板库,Standard Template Library)中的内容,STL提供了许多方便我们开发中使用的工具。

为了避免不必要的拷贝构造,我们将vector内部存储的元素定义为IMAGE类型的指针:vector<IMAGE*> -> IMAGE*[]。这里,二者的主要区别是,vector是一个根据元素数量动态增长的容器,而不需要像数组那样一开始便固定其容量大小。

  1. 我们将动画帧序列的vector容器定义为私有成员;

  2. 加载图片的部分自然就需要放在构造函数里面。这里抽象一下加载动画所需要的参数Animation(LPCTSTR path,int num,int inteval)分别是:图片文件包含的路径、当前动画所使用的图片数量和帧间隔(由于在目前的动画中,帧与帧之间的时间间隔是固定的);

  3. 循环加载图片。由于我们使用的图片素材命名都十分规律,所以可以直接将路径参数当作字符串格式化的模板;最后,我们将图片对象的指针添加到vector容器中,即:

    TCHAR path_file[256];
    for(size_t i = 0;i < num;i ++)
    {
        _stprintf_s(path_file,path,i);
        
        IMAGE* frame = new IMAGE();
        loadimage(frame,path_file);
    }
    

    注意:由于我们的vector内部存储的元素定义为IMAGE类型的指针,所以我们这里使用了new关键字来开辟内存。

标签:动画,int,Re0,pos,C++,玩家,player,从零开始,我们
From: https://blog.csdn.net/2301_80089588/article/details/139251197

相关文章

  • C/C++mai函数的参数
    在C和C++编程中,main函数通常是程序的入口点,定义程序的启动方式。函数签名intmain(intargc,constchar**argv,constchar**envp)包括三个参数:argc、argv和envp。这些参数分别用于接收命令行参数和环境变量。1.intargcargc代表“argumentcount”,表示传递给程序的命令行参......
  • 从零开始学习Frida Hook
    参考文章:https://www.jianshu.com/p/c349471bdef71、概述:Frida是个轻量级别的hook框架(没咋看懂)是PythonAPI,但JavaScript调试逻辑Frida的核心是用C编写的,并将Google的V8引擎注入到目标进程中,在这些进程中,JS可以完全访问内存,挂钩函数甚至调用进程内的本机函数来执行。使用Pytho......
  • c++ 多态整理笔记
    在C++中,多态性(polymorphism)是一种面向对象编程的特性,允许不同的对象通过相同的接口调用不同的实现。多态性主要通过虚函数来实现,使得基类的指针或引用可以指向派生类的对象,并调用派生类的重写函数。实现多态性的关键步骤声明虚函数:在基类中声明虚函数。重写虚函数:在派生类中......
  • C++课程设计实验杭州电子科技大学ACM题目(下)
    题目七:2060.Snooker题目描述ProblemDescription:background:PhiliplikestoplaytheQQgameofSnookerwhenhewantsarelax,thoughhewasjustalittlevegetable-bird.Maybeyouhadn'tplayedthatgameyet,nomatter,I'llintroducetheruleforyo......
  • C++实现自定义容器类型的范围循环
    先看一下类的设计与实现:classMyStack{public:MyStack()=default;MyStack(int*p,size_tlen):d(p),size(len){}int*begin(){returnd;}int*end(){return&d[size];}private:int*d=nullptr;size_tsize......
  • C++多线程原理详解
    学习C++多线程时,我有如下疑问:mutex的lock和unlock做了什么?mutex、lock_guard、unique_lock,它们之间的关系是什么?condition_variable中的wait做了什么?带着这些疑问,我查阅了一些资料,整理出本文。文章目录一、mutex二、lock_guard三、unique_lock四、condition......
  • 从C++示例理解开闭原则
    开闭原则要求我们在编写代码时,尽量不去修改原先的代码,当出现新的业务需求时,应该通过增加新代码的形式扩展业务而不是对原代码进行修改。假如我们现在有一批产品,每个产品都具有颜色和大小,产品其定义如下:enumclassColor{Red,Green,Blue};enumclassSize{Small,M......
  • C++:细谈Sleep和_sleep
    ZINCFFO的提醒还记得上上上上上上上上上上上上上上上上上上(上的个数是真实的)篇文章吗?随机应变——Sleep()和_sleep()但在ZINCFFO的C++怪谈-02中:我不喜欢Sleep......奤?媜煞鷥!整活!Sleep()是个什么东东?    Sleep()在windows.h和graphics.h里面都有。voidSlee......
  • [21] C++ 虚幻引擎项目结束
    Week21Day1大纲准备开始游戏踢除玩家根据职业更改外观样式内容踢除下线在玩家客户端调用让当前客户端下线,会退到默认地图voidAHallPlayerState::Client_AskLogout_Implementation(){ //下线 UKismetSystemLibrary::ExecuteConsoleCommand(this,TEXT("DISCONNECT")......
  • 《C++primer》读书笔记---第九章:顺序容器
    9.1顺序容器概述下表列出了标准库的顺序容器,所有容器都提供了快速顺序访问元素的能力:多种容器中,通常使用vector是最好的选择,除非你有很好的理由选则其他容器。以下是一些选择容器的基本原则:除非你有很好的理由选择其他容器,否则选择vector如果你的程序有很多小的元素,且空......