控制台游戏框架,以 “射击游戏”为例

shhoter console game program

Posted by xuepro on May 28, 2018

控制台游戏console game

现代计算机的屏幕或屏幕区域是由许多很小的“像素点”构成的,每个像素点可显示很多种颜色,这种计算机屏幕称为彩色显示器。在这种彩色显示器上可以显示各种色彩丰富的图形图像,比如我们的操作系统现在都是基于这种彩色图像构成各种具体图像元素的所谓“图形用户界面GUI”操作系统,这种图形系统上可以借助于图形库自由地在任何位置绘制各种图形图像。而早期的计算机只能“从上到下,从左到右”显示ASCII字符,即使在这种简陋的早期计算机上,聪明的程序员们也能制作出各种酷炫的字符型图形图像效果,比如开发出各种字符型游戏,用各种字符来表示各种图形。

这2天让学生学习我的朋友河海大学童晶老师的“做游戏、学C语言”的课程,我自己也顺便看了一下,发现这种讲课模式非常好、非常受学生欢迎。尽管之前, 童老师和我说他一直用做游戏方式教学生学C语言,我一直没在意。

我今天早上以他书(C语言课程设计与游戏开发实践教程》)中的“射击游戏”为示例写了一个控制台模式下的字符游戏框架,其中我方的战机可以发射子弹射击敌人(敌机)。

,这个游戏框架“麻雀虽小、五脏俱全”,所有的游戏都是采用的这种框架。希望小朋友进一步在此基础上改进并添加更多功能,如设计者可以连续发射子弹、敌机可以追击我方战机、多个敌机可能随机出现、奖励和惩罚(血量改变、武器升级)、更多种类的精灵等,学习C++的可以采用面向对象设计将各种精灵或对象(如画布(canvas)、各种精灵(如射击者shooter、子弹bullet、敌人enemy等))用类进行封装,并用C++的colletion类如vector等管理精灵和对象(敌人、子弹)等。

想进一步学习更多精彩酷炫的C++游戏编程技术,可以参加我的暑假的6天C++小白做游戏线下速成班 。具体报名可通过微博“教小白静态编程”或加QQ群:101132160。

还可以参考7年前我上课时的本科生写的比如纸牌类Desk(贪吃蛇程序,写出各种各样的控制台字符游戏!

游戏程序的框架

我们知道一个游戏就是一个随时间变化的画面,每一时刻的画面包括背景图像和一些动态物体(称为精灵)的图像。 游戏一开始会进行一些初始化工作,然后显示开始画面,根据用户的输入游戏中的元素(对象)会发生变化,从而导致画面产生变化。 游戏的过程通常一直循环地“处理用户输入、更新游戏的数据、绘制场景”,因此,所有游戏具有如下的程序结构或框架:

int main()
{
     //初始化
     
     setup();

     while(1){
	processInput();   //处理用户输入
	
	update();         //更新游戏数据
	
	renderScene();    //绘制场景
	
	show();           //显示游戏画面(即图像)—
     }
     
     return 0;
}

游戏的场景需要在一个游戏窗口的绘制屏幕上进行绘制,对于现代的游戏,绘制屏幕可以看成一幅图像,比如我们的电脑屏幕就是一个绘制屏幕,操作系统在上面绘制各种图标。即形成所谓的GUI用户界面。这个屏幕是具有一定分辨率(比如1024*768)的画板或画布,也就是画布由一些所谓的“像素”构成的,每个像素可以具有不同的颜色,我们只要对这些像素设置相应的颜色就可以表示各种物体。

控制台游戏(Console Game) 中没有通常的图形(图像)的那种像素式绘制屏幕(画布),其中显示的最小单位不是我们通常的屏幕“像素”而是“字符”,尽管如此,我们也可以将控制台窗口看成一块画布(Canvas),而每个位置能显示各种字符,这些位置的字符可以看成“像素”。

画布

1. 定义画布

因此,我们控制台游戏的窗口可以看成“颜色是各种字符”的画布。一个画布主要包括长、宽和存储每个像素的画布空间。因此,我们可以如下的画布:

//==========画布==========

 const int canvas_width = 50,canvas_height=20; //画布canvas的长宽
 
 char canvas[canvas_height][canvas_width];     //画布内容是一个“像素是字符”的矩阵(矩形区域)

定义画布中”像素”的常用颜色

我们可以用宏定义来定义画布的其他属性,比如背景颜色、边框颜色等。

#define background_color '.'

#define boundary_color '+'

显示空白背景画布

游戏每一时刻的画面绘制,通常先清空整个画布,即显示一个没有任何对象的空的背景画布。

我们可以通过一个辅助函数来清空画布。

//---------清空画布--------

void clear_canvas(){
     for(int y = 0; y<canvas_height;y++)
		  for(int x = 0; x<canvas_width;x++)
              canvas[y][x] = background_color;
     int right  =canvas_width-1;
     for(int y = 0; y<canvas_height;y++){
        canvas[y][0] = boundary_color;
        canvas[y][right] = boundary_color;
     }
     int bottom  =canvas_height-1;
     for(int x = 0; x<canvas_width;x++){
        canvas[0][x] = boundary_color;
        canvas[bottom][x] = boundary_color;
     }
}

我们可以”输出作为颜色的字符”显示这个空白的画布.

void show(){
  for(int y = 0; y< canvas_height;y++){
     for(int x = 0; x< canvas_width;x++)
	  std::cout<<canvas[y][x];
      std::cout<<'\n'; 
}
	

下面这个程序在renderScene中绘制只有空白背景的画布,然后通过show函数显示绘制好的场景图像。

#include <iostream>

using namespace std;

//---------常用的颜色-------

#define background_color '.'

#define boundary_color '+'


//==========画布==========

 const int canvas_width = 50,canvas_height=20; //画布canvas的长宽
 
 char canvas[canvas_height][canvas_width];     //画布内容是一个“像素是字符”的矩阵(矩形区域)
 
 
//---------清空画布--------

void clear_canvas(){
     for(int y = 0; y<canvas_height;y++)
		  for(int x = 0; x<canvas_width;x++)
              canvas[y][x] = background_color;
     int right  =canvas_width-1;
     for(int y = 0; y<canvas_height;y++){
        canvas[y][0] = boundary_color;
        canvas[y][right] = boundary_color;
     }
     int bottom  =canvas_height-1;
     for(int x = 0; x<canvas_width;x++){
        canvas[0][x] = boundary_color;
        canvas[bottom][x] = boundary_color;
     }
}


void setup(){
  
}

void processInput(){
}

void update(){
}

void renderScene(){
   clear_canvas();
}

void show(){
  for(int y = 0; y< canvas_height;y++){
     for(int x = 0; x< canvas_width;x++)
	    std::cout<<canvas[y][x];
     std::cout<<'\n';
  }
}

		
int main(){
	setup();

	while(1){
		processInput();
		update();
		renderScene();
	 	show();
	}
  

  return 0;
}

当我们运行这个程序时,会显示如下的画面

光标定位gotoxy :相当于清屏

由于while循环一直在运行,这个show显示的画面会一直往下。解决这个问题的办法是每次显示画布前,先光标定位到(0,0)的位置。 需要用到如下的光标定位函数:


//--------光标定位---------

#include <windows.h>

void gotoxy(int x, int y){
    COORD coord = {x, y};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}

//-----show函数开始调用这个goto(0,0)----------

//---作用:相当于每次清空控制台窗口,重新绘制

void show(){
  gotoxy(0,0)
  for(int y = 0; y< canvas_height;y++){
     for(int x = 0; x< canvas_width;x++)
	    std::cout<<canvas[y][x];
     std::cout<<'\n';
  }
}

重新运行修改后的程序,显示如下的画面

隐藏光标

我们看到此时画面上有光标在干扰,可以在程序一开始(setup函数中)调用如下的隐藏光标函数


//--------隐藏光标---------

void hideCursor(){
 CONSOLE_CURSOR_INFO cursor_info = {1, 0};
 SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}

void setup(){
     hideCursor();  
}

再运行修改后的程序,将显示干净的画面:

添加精灵 (战机、敌人)

下面我们在画布上添加一些精灵,比如添加一个战机、一个敌人,我们首先需要一些变量表示这些对象的位置和颜色。

//---颜色------

#define enemy_color '@'

#define shooter_color '*'

//------敌人位置------

int  enemy_x,enemy_y;

//------战机位置------

int shooter_x,shooter_y;

我们可以在setup函数中初始化战机和敌人的位置

void setup(){
     hideCursor();
	 enemy_x = canvas_width/2;
	 enemy_y = 2;

	 shooter_x = canvas_width/2;
	 shooter_y  = canvas_height-2;    
}

我们在每次绘制场景的函数renderScene()中绘制背景和战机、敌人

//---------绘制场景--------

void renderScene(){
    clear_canvas();

  //-------绘制enemy------
  
   if(enemy_x>=0&&enemy_x<canvas_width &&enemy_y>=0&&enemy_y<canvas_height)
		canvas[enemy_y][enemy_x] =  enemy_color;

  //-----绘制shooter-----------
  
    canvas[shooter_y-1][shooter_x] = shooter_color;
    canvas[shooter_y][shooter_x-1] = shooter_color;
    canvas[shooter_y][shooter_x] = shooter_color;
    canvas[shooter_y][shooter_x+1] = shooter_color;
    canvas[shooter_y+1][shooter_x-1] = shooter_color;
    canvas[shooter_y+1][shooter_x+1] = shooter_color;
}

上面的shooter因为具有超过一个“像素”的如下形状,所以用了多条绘制语句

        *
      * * *
      *   *

另外,我们修改背景颜色为空格' '

  #define background_color ' '

最后显示的画面如下

输入处理

我们希望当用户按下某个按键,比如空白键‘ ’ 时,战机能够发射子弹。当用户按下比如’a’,’d’.’w’,’s’等按键时,能“左右上下”移动战机的位置。

检测用户是否按下某按键可以用头文件的kbhit(),并用getch()得到按键字符。

首先,我们需要定义一个子弹

#define bullet_color '|'

int  bullet_x = -1,bullet_y = -1;

当用户按下空格键’ ‘时,生成子弹的位置正好在战车的上方

//--------处理输入--------
void processInput(){
  char input;
  if(kbhit()){
	input = getch();
	if(input==' '){
	   //生成子弹的位置正好在战机的上方
	   
	   bullet_x = shooter_x;          bullet_y = shooter_y-3;
	}
	else if(input=='a'||input=='A'){
             if(shooter_x>0)  shooter_x--;  //战机左移
	     
	     
	}
	else if(input=='d'||input=='D'){
             if(shooter_x<canvas_width-1)      shooter_x++;   //战机右移
	     
	}
	else if(input=='w'||input=='W'){
              if(shooter_y>0)            shooter_y--;
	}
	else if(input=='s'||input=='S'){
              if(shooter_y<canvas_height-1)	       shooter_y++;
	}
   }

}

在renderScene里添加绘制子弹的代码

void renderScene(){
    clear_canvas();

    if(enemy_x>=0&&enemy_x<canvas_width &&enemy_y>=0&&enemy_y<canvas_height)
		canvas[enemy_y][enemy_x] =  enemy_color;

    canvas[shooter_y-1][shooter_x] = shooter_color;
	canvas[shooter_y][shooter_x-1] = shooter_color;
	canvas[shooter_y][shooter_x] = shooter_color;
	canvas[shooter_y][shooter_x+1] = shooter_color;
	canvas[shooter_y+1][shooter_x-1] = shooter_color;
	canvas[shooter_y+1][shooter_x+1] = shooter_color;

	if(bullet_x>=0&&bullet_x<canvas_width&&bullet_y>=0
       &&bullet_y<canvas_height)
		canvas[bullet_y][bullet_x] =  bullet_color;

}

运行修改的程序,当我们按下’a’,’d’,’w’,’s’时,战机会移动,当我们按下空格键后,会出现一个子弹。但子弹静止不动,要使得子弹前进,我们需要修改update函数。如图


//--------更新数据--------

void update(){
  //存在子弹时,每次重新绘制画面前,子弹位置向上移动一个位置
  
  if(bullet_y>1) bullet_y--;
}

再次运行这个修改后的程序,我们按下空格键后,子弹会向上前进!

完整代码如下:

#include <iostream>

#include <windows.h>

#include <conio.h>

using namespace std;


//====辅助函数=========

//------光标定位-------

void gotoxy(int x, int y){
    COORD coord = {x, y};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}

//------隐藏光标-------

void hideCursor(){
 CONSOLE_CURSOR_INFO cursor_info = {1, 0};
 SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}


//---------常用的颜色-------

#define background_color ' '

#define boundary_color '+'
//---颜色------

#define enemy_color '@'

#define shooter_color '*'

#define bullet_color '|'

//------敌人位置------

int  enemy_x,enemy_y;

//------战机位置------

int shooter_x,shooter_y;

//------子弹位置------

int  bullet_x = -1,bullet_y = -1;

//==========画布==========

 const int canvas_width = 50,canvas_height=20; //画布canvas的长宽

 char canvas[canvas_height][canvas_width];     //画布内容是一个“像素是字符”的矩阵(矩形区域)


//---------清空画布--------

void clear_canvas(){
     for(int y = 0; y<canvas_height;y++)
		  for(int x = 0; x<canvas_width;x++)
              canvas[y][x] = background_color;
     int right  =canvas_width-1;
     for(int y = 0; y<canvas_height;y++){
        canvas[y][0] = boundary_color;
        canvas[y][right] = boundary_color;
     }
     int bottom  =canvas_height-1;
     for(int x = 0; x<canvas_width;x++){
        canvas[0][x] = boundary_color;
        canvas[bottom][x] = boundary_color;
     }
}



void setup(){
     hideCursor();
     enemy_x = canvas_width/2;
	 enemy_y = 2;

	 shooter_x = canvas_width/2;
	 shooter_y  = canvas_height-3;

}

//--------处理输入--------

void processInput(){
  char key;
  if(kbhit()){
	key = getch();
	if(key==' '){
	   //生成子弹的位置正好在战机的上方

	   bullet_x = shooter_x;
	   bullet_y = shooter_y-3;
	}
	else if(key=='a'||key=='A'){
             if(shooter_x>0)  shooter_x--;  //战机左移
	     

	}
	else if(key=='d'||key=='D'){
             if(shooter_x<canvas_width-1)      shooter_x++;   //战机右移
	     
	}
	else if(key=='w'||key=='W'){
              if(shooter_y>0)            shooter_y--;
	}
	else if(key=='s'||key=='S'){
              if(shooter_y<canvas_height-1)	       shooter_y++;
	}
   }

}

//--------更新数据--------

void update(){
  //存在子弹时,每次重新绘制画面前,子弹位置向上移动一个位置
  
  if(bullet_y>1) bullet_y--;
}

void renderScene(){
    clear_canvas();

    if(enemy_x>=0&&enemy_x<canvas_width &&enemy_y>=0&&enemy_y<canvas_height)
		canvas[enemy_y][enemy_x] =  enemy_color;

    canvas[shooter_y-1][shooter_x] = shooter_color;
	canvas[shooter_y][shooter_x-1] = shooter_color;
	canvas[shooter_y][shooter_x] = shooter_color;
	canvas[shooter_y][shooter_x+1] = shooter_color;
	canvas[shooter_y+1][shooter_x-1] = shooter_color;
	canvas[shooter_y+1][shooter_x+1] = shooter_color;

	if(bullet_x>=0&&bullet_x<canvas_width&&bullet_y>=0
       &&bullet_y<canvas_height)
		canvas[bullet_y][bullet_x] =  bullet_color;

}


void show(){
  gotoxy(0,0);
  for(int y = 0; y< canvas_height;y++){
     for(int x = 0; x< canvas_width;x++)
	    std::cout<<canvas[y][x];
     std::cout<<'\n';
  }
}


int main(){
	setup();

	while(1){
		processInput();
		update();
		renderScene();
	 	show();
	}


  return 0;
}

上述程序还有许多问题和改进的地方:

  1. 只有一个子弹,如果连续快速按空格键,会看到子弹总是反复在战机附近,没法前进。解决办法是用一个数组存储不断发射的子弹

  2. 敌机位置固定,应该让敌机位置随机出现。

  3. 敌机数量单一,应该随机生成很多敌机

  4. 敌机应该能否追击撞击我方战机 或者 敌机也能发射子弹

  5. 当子弹射中敌机时,没有效果,是否应该增加我方的分数

  6. 当我方血量增大时,是否可以升级武器?

  7. 可否改成面向对象设计方式? 定义一个抽象的精灵类,从它派生出各种具体的类如:战机、敌机、子弹等等?


支付宝打赏 微信打赏

您的打赏是对我最大的鼓励!