割草游戏,有玩家(上下左右控制移动)周围围绕子弹,敌人(随机刷新)向玩家靠近,子弹打死敌人,玩家与敌人触碰游戏结束。
分析需求
1.有玩家、敌人、子弹三种对象
2.玩家上下左右控制、左右移动式玩家朝向不同
3.敌人从边框随机出现、向玩家移动
4.子弹与敌人有碰撞检测、敌人与玩家有碰撞检测
图片素材加载
在之前的笔记中使用的是绘画的线条函数进行的简单游戏,这次我们来制作由图片构成的游戏。关键在于两个函数(EasyX怎么安装看之前笔记)
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
putimage(0, 0, &ImgBackground);
在#include <graphics.h>文件下有IMAG类让我们可以操作图片素材。
loadimage第一个参数为指针名,第二个参数为文件实际存放目录,其将文件名变为指针变量可供我们使用。
putimage函数将这个图片的左上角放在界面的(0,0)位置。
图片素材的目录存放在如图位置,img文件存放了游戏所需要的所有游戏素材,点进img即可看见图片素材
我们有背景素材、 野猪向左和向右的6帧动画、主角的向左向右帧动画等等。提前对素材特点命名方便代码使用。
至此你已经会了加载图片素材。
#include <graphics.h>
bool GameOver = false;
int main()
{
initgraph(1280, 720);
ExMessage msg;
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
while (!GameOver)
{
while (peekmessage(&msg))
{
}
putimage(0, 0, &ImgBackground);
}
}
创建1280x720画布,image创建图片类、exmessage创建消息类,loadimage将文件目录与指针对应。
第一个while循环就是游戏进程,跳出第一个循环即为游戏结束我们常创建GameOver标志作为标记,当在游戏过程中达到某种胜利条件时将GameOver置为true游戏即退出。
第二个while循环代表事件循环,一直扫描键鼠消息。
putimage函数就是将指针代表的图片左上角放置在(0,0)位置上。
此时加载图片还是有些卡顿,我们加入以下代码
#include <graphics.h>
bool GameOver = false;
int main()
{
initgraph(1280, 720);
ExMessage msg;
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
BeginBatchDraw();
while (!GameOver)
{
while (peekmessage(&msg))
{
}
putimage(0, 0, &ImgBackground);
FlushBatchDraw();
}
EndBatchDraw();
}
BeginBatchDraw函数用于开始批量绘图。执行后,任何绘图操作都将暂时不输出到绘图窗口上,直到执行 FlushBatchDraw 或 EndBatchDraw 才将之前的绘图输出。
玩家加载
至此背景你已经会实现了,接下来我们来加载玩家
在图片素材文件下我们看到派蒙的帧动画命名很有规律。两类一类向左一类向右序号从0到5,我们只需要改变数字即可。
让玩家动起来就是轮播这6帧动画
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
putimage(0, 0, &ImgBackground);
在背景图中我们使用了loadimage函数,其第二个参数表示图片位置我们修改即可
_T表示字符需要用unicode字符集表示
打开项目的属性在字符集处勾选unicode字符集
IMAGE ImgPlayerLeft;
loadimage(&ImgPlayerLeft, _T("img/player_left_0.png"));
putimage(0, 0, &ImgPlayerLeft);
但是这仅仅加载了派蒙的0号帧,怎么在目录字符串中修改012345呢?字符串拼接
将img/player_left_0.png拆分为“img/player_left_” + i +“.png”三个部分放在循环内修改i的值将其拼凑起传入loadimage函数。
此时我们用到了<string>文件中函数
#include <graphics.h>
#include <string>
bool GameOver = false;
const int PLAYER_ANIM_NUM = 6;
IMAGE ImgPlayerLeft[PLAYER_ANIM_NUM];
IMAGE ImgPlayerRight[PLAYER_ANIM_NUM];
void LoadAnimation()
{
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_left_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerLeft[i], path.c_str());
}
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_right_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerRight[i],path.c_str());
}
}
我们定义path为“img/player_left_” + i +“.png”
第一,字符串前加L,将ASCII转为Unicode码,就是说“abc”占三个字节L“abc”占六个字节。
第二,to_wstring函数是将int型转为宽字符字符串
第三,+是文件中已经有对“+”的重载,所以可以进行字符串运算
第四,c_str函数将const string* 类型 转化为 const char* 类型,将其转化为字符串数组
#include <graphics.h>
#include <string>
bool GameOver = false;
const int PLAYER_ANIM_NUM = 6;
IMAGE ImgPlayerLeft[PLAYER_ANIM_NUM];
IMAGE ImgPlayerRight[PLAYER_ANIM_NUM];
int IdxCurrentAnim = 0;
void LoadAnimation()
{
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_left_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerLeft[i], path.c_str());
}
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_right_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerRight[i],path.c_str());
}
}
int main()
{
initgraph(1280, 720);
ExMessage msg;
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
LoadAnimation();
BeginBatchDraw();
while (!GameOver)
{
while (peekmessage(&msg))
{
}
static int counter = 0;
if (++counter%5==0)
{
IdxCurrentAnim++;
}
IdxCurrentAnim = IdxCurrentAnim % PLAYER_ANIM_NUM;
cleardevice();
putimage(0, 0, &ImgBackground);
putimage(500, 500, &ImgPlayerLeft[IdxCurrentAnim]);
FlushBatchDraw();
}
EndBatchDraw();
}
在main函数下我们使用counter计数器,我们用%5或者其他数字也可以,控制IdxCurrentAnim的增长速度,就是控制帧动画的播放速率。
此时派蒙在这里抽搐,我们将%5换为%100就舒服多了。
还有一个问题,黑框怎么办?
第一种方法自己在ps等修图工具间裁剪出派蒙的边界,显然工作量略大。
第二种我们利用rgb配色修改纯黑位置
#pragma comment(lib,"MSIMG32.LIB")
inline void putimageA(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 });
}
我们编写一个函数putimageA函数,其功能就是去掉黑色背景,inline关键字就是为了在while重复调用中减少栈的开销,类似“宏”的操作。将putimageA放入main函数中。
#include <graphics.h>
#include <string>
bool GameOver = false;
const int PLAYER_ANIM_NUM = 6;
IMAGE ImgPlayerLeft[PLAYER_ANIM_NUM];
IMAGE ImgPlayerRight[PLAYER_ANIM_NUM];
int IdxCurrentAnim = 0;
#pragma comment(lib,"MSIMG32.LIB")
inline void putimageA(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 });
}
void LoadAnimation()
{
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_left_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerLeft[i], path.c_str());
}
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_right_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerRight[i],path.c_str());
}
}
int main()
{
initgraph(1280, 720);
ExMessage msg;
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
LoadAnimation();
BeginBatchDraw();
while (!GameOver)
{
while (peekmessage(&msg))
{
}
static int counter = 0;
if (++counter%100==0)
{
IdxCurrentAnim++;
}
IdxCurrentAnim = IdxCurrentAnim % PLAYER_ANIM_NUM;
cleardevice();
putimage(0, 0, &ImgBackground);
putimageA(500, 500, &ImgPlayerLeft[IdxCurrentAnim]);
FlushBatchDraw();
}
EndBatchDraw();
}
此时代码运行效果为
玩家移动
键盘上下左右控制,我们要习惯设置标志位,按下弹起两个状态
POINT player_pos = { 500,500 };
const int PLAYER_SPEED = 10;
bool is_move_up = 0;
bool is_move_down = 0;
bool is_move_left = 0;
bool is_move_right = 0;
设置图片初始位置500,500(自带的位置结构体POINT)、设置速度、方向标志位置零。
接下来写事件函数,扫描键盘按键信息,根据情况改变标志位并退出switch语句。
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)player_pos.y -= PLAYER_SPEED;
if (is_move_down)player_pos.y += PLAYER_SPEED;
if (is_move_left)player_pos.x -= PLAYER_SPEED;
if (is_move_right)player_pos.x += PLAYER_SPEED;
如果标志位为真,则对玩家坐标进行计算,对于坐标不太熟悉同学可以看井字棋补课。总的说画布左上角(0,0)右下角(1280,720),从左到右x变大、从上到下y变大。
记得将putimageA函数坐标改变
putimageA(player_pos.x, player_pos.y, &ImgPlayerLeft[IdxCurrentAnim]);
此时玩家的坐标就不再是(500,500)定点了,可以随着计算出的数值移动。此时你会发现派蒙窜的飞快。原因是按下就走走走走走走走一直不停的累加,就显得飞快,只要在之间加入sleep延时即可按下走、走、走。
那么如何把握sleep的时间长度呢?我们知道sleep(1000)表示延时1s,sleep(1000/144)表示的就是1000/144为1帧,在一帧内派蒙存在时间加上延时时间为一帧即可。
实现方法就是
while (!GameOver)
{
DWORD StartTime = GetTickCount();
while (peekmessage(&msg))
在事件函数循环前记录时间
DWORD EndTime = GetTickCount();
DWORD DeleteTime = EndTime - StartTime;
if (DeleteTime <1000/144)
{
Sleep(1000 / 144 - DeleteTime);
}
FlushBatchDraw();
在结尾时记录结束时间,并根据时间差补全一帧内应延时的时间。
DWORD是EasyX带有的无符号int的别名,在这里是为了更容易分辨这两个位置是实现同功能的代码。
至此角色移动你已经学会了。
#include <graphics.h>
#include <string>
bool GameOver = false;
const int PLAYER_ANIM_NUM = 6;
IMAGE ImgPlayerLeft[PLAYER_ANIM_NUM];
IMAGE ImgPlayerRight[PLAYER_ANIM_NUM];
int IdxCurrentAnim = 0;
POINT player_pos = { 500,500 };
const int PLAYER_SPEED = 10;
bool is_move_up = 0;
bool is_move_down = 0;
bool is_move_left = 0;
bool is_move_right = 0;
#pragma comment(lib,"MSIMG32.LIB")
inline void putimageA(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 });
}
void LoadAnimation()
{
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_left_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerLeft[i], path.c_str());
}
for (int i = 0; i < PLAYER_ANIM_NUM; i++)
{
std::wstring path = L"img/player_right_" + std::to_wstring(i) + L".png";
loadimage(&ImgPlayerRight[i],path.c_str());
}
}
int main()
{
initgraph(1280, 720);
ExMessage msg;
IMAGE ImgBackground;
loadimage(&ImgBackground, _T("img/background.png"));
LoadAnimation();
BeginBatchDraw();
while (!GameOver)
{
DWORD StartTime = GetTickCount();
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)player_pos.y -= PLAYER_SPEED;
if (is_move_down)player_pos.y += PLAYER_SPEED;
if (is_move_left)player_pos.x -= PLAYER_SPEED;
if (is_move_right)player_pos.x += PLAYER_SPEED;
static int counter = 0;
if (++counter%100==0)
{
IdxCurrentAnim++;
}
IdxCurrentAnim = IdxCurrentAnim % PLAYER_ANIM_NUM;
cleardevice();
putimage(0, 0, &ImgBackground);
putimageA(player_pos.x, player_pos.y,&ImgPlayerLeft[IdxCurrentAnim]);
DWORD EndTime = GetTickCount();
DWORD DeleteTime = EndTime - StartTime;
if (DeleteTime <1000/144)
{
Sleep(1000 / 144 - DeleteTime);
}
FlushBatchDraw();
}
EndBatchDraw();
}
至此完成成了一半的任务了,下一步将Player封装为类将与之相关的函数与变量都丢进去。