一.游戏的展示效果
二.本节开发日志
三.素材
在主页有放置
四.游戏实现
1)创建C++项目将素材导入项目
#include <iostream>
using namespace std;
int main(){
return 0;
}
2)游戏初始化
对于新手而言,建议拿到游戏的时候,从游戏的背景图进行入手,由于在游戏边玩边从磁盘加载资源,这样会影响游戏的体验,这也就是为什么王者荣耀进去时候要加载的原因,都是在开始游戏之前先把资源加载到项目里面,我们就可以在游戏开始之前进行创建一个用于初始化的函数,用于专门加载这些资源
1.实现游戏窗口的创建与游戏背景的加载
#include <iostream>
#include <graphics.h>
//窗口的大小,一般在开发中由项目经理规定,定义为宏不仅可以增强代码的可读性还可以增强代码的健壮性
#define WIN_WIDTH 1012
#define WIN_HEIGHT 396
using namespace std;
IMAGE imgBg[3];
void init(){
//创建游戏窗口
initgraph(WIN_WIDTH, WIN_HEIGHT);
//加载游戏背景
char name[64];
for (int i = 0; i < 3; i++) {
//"res/bg001.png res/bg002.png
sprintf_s(name, "res/bg%03d.png", i + 1);
loadimage(&imgBgs[i], name);
}
}
int main(){
init();//用于初始化
return 0;
}
2.渲染游戏背景图,可以创建一个专门渲染图片的函数updataBg()
由于由三层,每层的y坐标都是不同的,y坐标可以测量出来,由于背景需要滚动,原理是移动当前背景图,使其向左移动,当图片的最右边达到游戏窗口的最左边时候,重新复原图片的位置,使图片的最左边到达窗口的最左边,进行循环,循环的时候,三层图片以不同的速度进行移动,所以创建一个数组bgSpeed,用于存放三个图片的速度,bgX数组用于存放当前的x值,由于游戏是个死循环,所以除了初始化,其余函数套在while(1)循环里
int bgSpeed[3] = {1,2,4};
int bgX[3];//背景图片的x坐标
//渲染游戏背景
void updateBg() {
putimagePNG2(bgX[0], 0, WIN_WIDTH,&imgBgs[0]);
putimagePNG2(bgX[1], 119,WIN_WIDTH, &imgBgs[1]);
putimagePNG2 (bgX[2], 330, WIN_WIDTH,&imgBgs[2]);
}
void init(){
//for循环里,添加bgX[i] = 0 ;给变量进行初始化
}
int main(){
'''
init();
while(1){
updateBg();
}
...
}
3.由于上面渲染的渲染背景图还是静态的,没有移动,因为bgx并没有改变,此时需要一个专门改变游戏数据的函数,fly函数吧
void fly(){
for(int i = 0 ;i<3;i++){
bgX[i] -=bgSpeed[i];//使每层图片减去他对应的速度
//做一个判断,当达到最左边时候,复位图片的位置
if(bgX[i] <=-WIN_WIDTH){
bgX[i] = 0;
}
}
}
4.此时的背景图已经开始滚动了,但是还有个小bug,因为这个渲染时用循环putimage上去的,就相当于将图片打印到上面,这也会出现的情况是每层图片之间会一闪一闪的,解决这个问题的方法,是先将他们输出到缓存区,一并打印出来,用到的函数如下:
此时就解决了背景的第一个问题
背景图实现,程序的进度:
3)创建玩家
1.由于图片在动,只需要将人物在原地就行,将人物放在最中间,由于要实现人物的运动,这个的原理是将人物的多组图片进行轮播,人物的x,y坐标可以通过数学进行算出来,此时我们先在初始化函数里加载图片,在初始化函数里计算出人物的x,y坐标,由于人物的轮播,可以传创建一个帧序列
//人物的背景图
IMAGE imgHero[12];
int heroX;
int heroY;
int heroIndex;//帧序号
void init(){
...
//加载人物的图片,仍然老套路,用到sprintf函数
for (int i = 0; i < 12; i++) {
sprintf(name, "res/hero%d.png", i + 1);
loadimage(&imgHero[i], name);
}
//人物初始化
heroX = WIN_WIDTH * 0.5 - imgHero[0].getwidth() * 0.5;
heroY = 345 - imgHero[0].getheight();
heroIndex = 0;
}
由于帧序列要改变才能实现轮播,所以需要在数据层的fly函数,进行修改帧序列
void fly(){
....
//修改帧序列,
/*heroIndex++;//如果使用这种方法的话,当进行越界,我们需要的是从1-12进行循环,所以我们用取余数进行实现
*/
heroIndex = (heroIndex+1)%12;
}
数据也可也可以改变了,我们需要将人物的图片渲染在窗口中,在main函数中只需要一条代码
int main(){
...
//实现人物的奔跑
putimagePNG2(heroX, heroY, WIN_WIDTH , &imgHero[heroIndex]);
...
}
效果图
这样人物就可以原地跑起来了,解决了第二个问题
4)实现玩家的跳跃
1.人物的跳跃是当玩家按下按键后,人物的y坐标开始上升,当达到某个高度后,然后下落
先来实现用户点击函数keyEvent(),由于只需要改变y坐标,底层的数据变化实在fly函数里面,我们只需要获取到用户的点击之后,给个可以改变y值得信号即可
void keyEvent(){
//先接收用户得鼠标消息
char ch = 0 ;
if(_kbhit()){
ch = _getch();
if(ch ===' '){
jump();
}
}
}
实现初始化顺便打开可跳跃得按钮
//全局变量
bool heroJump;//跳跃状态
int jumpHeightMax;//跳跃得最大高度
int heroJumpOff;//跳跃得偏移量
void init(){
...
//对跳跃得数据进行初始化
//跳跃初始化
heroJump = false;
jumpHeightMax = 345 - imgHero[0].getheight() - 120;
heroJumpOff = -4;
}
void jump(){
heroJump = true;
}
在fly函数里改变底层数据
void fly() {
...
//实现跳跃
if (heroJump) {
if (heroY < jumpHeightMax) {
heroJumpOff = 4;//+ (-4)等于向上走,+4等于向下走
}
heroY += heroJumpOff;
if (heroY > 345 - imgHero[0].getheight()) {
//达到地面
heroJump = false;
heroJumpOff = -4;
}
}
}
此时的英雄就可以进行跳跃了
这样人物就可以跳跃起来了,就能跳跃障碍物了,解决了第三个问题,此时还面临一个小bug,就是在当跳跃的途中,这个人物还处于一个跑步的状态,影响了用户体验,解决这个问题,我们就可以在改变底层数据里改部分代码,如果处于起跳状态则不更新帧序列,否则不跳的时候更新帧序列,以下是改变之后的渲染代码:
//人物跳跃
if (heroJump) {
if (heroY < jumpHeightMax) {
heroJumpOff = 4;
}
heroY += heroJumpOff;
if (heroY > 345 - imgHero[0].getheight()) {
heroJump = false;
heroJumpOff = -4;
}
}
else {
//heroIndex++ 这种方式会越界
heroIndex = (heroIndex + 1) % 12;
}
这样就解决了这个小bug,以上实现了背景图的轮播,人物的动态化,和人物的起跳,主要学到了,将渲染层,数据层分开,其他小的功能进行分支化,将main函数写的简介,实现功能模块化,接下来就可以实现障碍物了
5)障碍物小乌龟的实现
1.障碍物有许多种,先实现一种,其他的在做优化,依旧是老套路,初始化里面加载资源,渲染层进行打印图片,fly里面改变数据
2.先将小乌龟设置为静态的,优化时候也可加个帧序列实现动态的
定义小乌龟的变量
//全局状态
//小乌龟
IMAGE imgTortoise;
//小乌龟存在开关
bool torToiseExist;
int torToiseX;
int torToiseY;
将小乌龟进行初始化
//初始化小乌龟
void init(){
...
//小乌龟初始化
loadimage(&imgTortoise, "res/t1.png");
torToiseY = 345 + 5 - imgTortoise.getheight();
torToiseExist = false;
}
从底层数据方面对小乌龟进行创建与使其移动
void fly(){
...
//创建小乌龟
static int frameCount = 0;
static int enemyFre = 100;
frameCount++;
if (frameCount > enemyFre) {
frameCount = 0;
enemyFre = 100+ rand() % 50;
//创建障碍物
if (!torToiseExist) {
torToiseExist = true;
torToiseX = WIN_WIDTH;
}
}
//使小乌龟的运动,运动应该于草坪的移动速度保持一致,才能显得静止在草坪上
if (torToiseExist) {
torToiseX -= bgSpeed[2];
if (torToiseX < -imgTortoise.getwidth()) {
torToiseExist = false;
}
}
}
将小乌龟渲染出来
void updateEnemy() {
//渲染小乌龟
if (torToiseExist) {
putimagePNG2(torToiseX, torToiseY,WIN_WIDTH, &imgTortoise);
}
}
将其函数在main函数中调用,和渲染人物一起渲染出来
此时代码进度为:
#include <iostream>
#include<graphics.h>
#include <conio.h>
#include "tools.h"
#define WIN_WIDTH 1012
#define WIN_HEIGHT 396
using namespace std;
/*
1.先做背景
2.创建玩家
3.实现玩家跳跃
*/
//背景
IMAGE imgBgs[3];
int bgSpeed[3] = { 1,2,4 };
int bgX[3];
//人物
IMAGE imgHero[12];
int heroX;
int heroY;
int heroIndex;//帧序列
//跳跃
bool heroJump;//跳跃状态
int jumpHeightMax;//跳跃的最大高度
int heroJumpOff;
//小乌龟
IMAGE imgTortoise;
//小乌龟存在开关
bool torToiseExist;
int torToiseX;
int torToiseY;
void init() {
//创建游戏窗口
initgraph(WIN_WIDTH, WIN_HEIGHT);
//加载背景图
char name[64];
for (int i = 0; i < 3; i++) {
//"res/bg001.png res/bg002.png
sprintf_s(name, "res/bg%03d.png", i + 1);
loadimage(&imgBgs[i], name);
bgX[i] = 0;
}
//加载人物背景图
for (int i = 0; i < 12; i++) {
sprintf(name, "res/hero%d.png", i + 1);
loadimage(&imgHero[i], name);
}
//人物的位置放在窗口的最中间,由于人物需要运动,用多组图片进行轮播,需要定义一个帧序列
heroX = WIN_WIDTH * 0.5 - imgHero[0].getwidth() * 0.5;
heroY = 345 - imgHero[0].getheight();
heroIndex = 0;
//跳跃
heroJump = false;
jumpHeightMax = 345 - imgHero[0].getheight() - 120;
heroJumpOff = -4;
//小乌龟初始化
loadimage(&imgTortoise, "res/t1.png");
torToiseY = 345 + 5 - imgTortoise.getheight();
torToiseExist = false;
}
void updataBg() {
putimagePNG2(bgX[0], 0, WIN_WIDTH, &imgBgs[0]);
putimagePNG2(bgX[1], 119, WIN_WIDTH, &imgBgs[1]);
putimagePNG2(bgX[2], 330, WIN_WIDTH, &imgBgs[2]);
}
void updateEnemy() {
//渲染小乌龟
if (torToiseExist) {
putimagePNG2(torToiseX, torToiseY,WIN_WIDTH, &imgTortoise);
}
}
void fly() {
//背景图
for (int i = 0; i < 3; i++) {
bgX[i] -= bgSpeed[i];
if (bgX[i] < -WIN_WIDTH) {
bgX[i] = 0;
}
}
//实现跳跃
if (heroJump) {
if (heroY < jumpHeightMax) {
heroJumpOff = 4;//+ (-4)等于向上走,+4等于向下走
}
heroY += heroJumpOff;
if (heroY > 345 - imgHero[0].getheight()) {
//达到地面
heroJump = false;
heroJumpOff = -4;
}
}
else {
//改变人物帧序列
heroIndex = (heroIndex + 1) % 12;
}
//创建小乌龟
static int frameCount = 0;
static int enemyFre = 100;
frameCount++;
if (frameCount > enemyFre) {
frameCount = 0;
enemyFre = 100+ rand() % 50;
//创建障碍物
if (!torToiseExist) {
torToiseExist = true;
torToiseX = WIN_WIDTH;
}
}
//使小乌龟的运动,运动应该于草坪的移动速度保持一致,才能显得静止在草坪上
if (torToiseExist) {
torToiseX -= bgSpeed[2];
if (torToiseX < -imgTortoise.getwidth()) {
torToiseExist = false;
}
}
}
void jump() {
//跳跃只需要改变y值即可,在底层数据管理函数实现,此时只需要给出可以改数据的信号即可
heroJump = true;
}
void keyEvent() {
//获取玩家键盘事件
char ch = 0;
if (_kbhit()) {
ch = _getch();
if (ch == ' ') {
jump();
}
}
}
int main() {
init();
while (1) {
keyEvent();
BeginBatchDraw();
//渲染背景
updataBg();
//渲染人物
putimagePNG2(heroX, heroY, &imgHero[heroIndex]);
//渲染障碍物
updateEnemy();
EndBatchDraw();
fly();
Sleep(30);
}
system("pause");
return 0;
}
6)对代码进行优化
1.在main函数的最后用了一条函数sleep(30),会将程序休眠30毫秒,如果在此期间有用户的键盘输入,会降低游戏的体验性
2.在创建小乌龟时候,由于只演示了一种障碍物,而添加障碍物时候,应该建立一个专门创建障碍物的函数