# planeGame **Repository Path**: lovesnsfi_admin/planeGame ## Basic Information - **Project Name**: planeGame - **Description**: 基于C#的winform平台的飞机大战 - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 4 - **Created**: 2019-05-25 - **Last Updated**: 2023-05-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 飞机大战开发文档 ### 项目信息 * 开发人员:杨标 * 项目名称:基于.net平台的游戏开发 * 项目周期:2019年5月19日 * 开发工具:Visual Studio 2012 ------------------ ### 开发规范 * 注意项目命名与窗体及变量命名 -------------------- ### 创建项目 在创建项目的时候,严格按照项目创建流程 1. 先创建解决方案 在这个地方,一定要注意解决方案的名称 2. 根据项目的需要,来创建项目 ### 设计主窗体 1. 更改窗体的名称为"MainForm" 2. 更改窗体的标题 改为:“传媒学院飞机大战” 3. 设置窗体大小 * 禁止窗体最大化 * 禁止改变窗体大小 `FormBoderStyle:FixedSingle` * 根据游戏的背景图来设置游戏窗体的大小宽度480,高度:850 ### 导入游戏资源 在C#项目里面,导入资源是一个经常会进行的操作,但是也有固定的格式 双击项目里面的`Properties`打开属性框,然后选资源,如下图所示 ![1558229430979](assets/1558229430979.png) 选择上面的添加资源-----添加现有资源,把所有的游戏资源导入进去 到此为止,我们的游戏准备工作就已经结束了 ------------ ### 游戏背景功能完成 通过刚刚的资源导入,我们已经发现有一个资源图片,现在,需要通过这个背景图完成我们的游戏背景功能 #### C#里面的游戏开发主要使用技术有那些 在很早以前,做游戏的时候,都是通过DX或openGL来进行的,但是在winform里面,这种游戏是通过一种叫做GDI+的东西来完成 > GDI+是什么? > > GDI:图形图像设备接口,可以利用这种技术在任何界面上面绘制任何东西 在winForm的窗体里面,提供一个方法用来在窗体上面去绘制东西 这一个方法就是`OnPaint`的方法 ```csharp protected override void OnPaint(PaintEventArgs e); ``` #### 背景图片绘制 ```csharp protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.DrawImage(Properties.Resources.background,0,0); } ``` > 代码说明: > > * 在PaintEventArgs 它有一个Graphics --------GDI当中的G 可以把它看成画笔 > > * e.Graphics 它是画笔 > > * DrawImage()调用绘画图片的方法 #### 背景图滚动 ![1558232406861](assets/1558232406861.png) 在窗体上面添加一个Timer控件,相当于一个闹钟,把上面的属性设置如所所示 * Interval代表这个闹钟每隔多长时间执行一次 * Enabled相当是否打开这个闹钟 ```csharp private void timer1_Tick(object sender, EventArgs e) { bgY = bgY + 5; if (bgY>0) { bgY = -850; } this.Invalidate(); //重新去绘画窗体 } ``` 当完成上面的代码以后,我们发现屏幕 在进行闪烁 ### 解决屏幕闪烁 屏幕闪烁的原因是因为这个时候屏幕上面的变化大快了,这个时候,我们需要使用一个缓冲过程 ```csharp public MainForm() { InitializeComponent(); this.SetStyle(ControlStyles.OptimizedDoubleBuffer|ControlStyles.AllPaintingInWmPaint|ControlStyles.ResizeRedraw,true); } ``` > * OptimizedDoubleBuffer添加缓冲 > * AllPaintingInWmPaint 防止窗体手动闪烁 > * ResizeRedraw 当窗体大小发生改变的时候 ### 玩家飞机的创建 我们要利用面向对象的特点来完成这个游戏的开发 > 在这个游戏当中,玩家飞机是个对象,敌人飞机是个对象 ,子弹是对象,所有的都是对象 > > 在编程语言里面,对象泛指`class` #### 创建玩家飞机的对象 * 新建一个class对象,取名`Hero` * 完成如下代码 ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; namespace whuc.PlaneGame { /// /// 玩家飞机的对象 /// public class Hero { /* 思考:这个对象里面,都有那些东西 飞机的图片 飞机的坐标 飞机的大小 在C#里面,定义属性的快捷键 prop */ public int X { get; set; } public int Y { get; set; } public Image img { get; set; } //如果要使用图片,那么,需要导命名空间System.Drawing; public int Width { get; set; } //飞机的宽度与高度 public int Height { get; set; } //创建一个构造方法,快速的初始化我们刚刚定义的属性 public Hero(int x,int y) { this.img = Properties.Resources.hero1; //玩家飞机图片赋值 this.X = x; this.Y = y; //飞机的宽度与高度应该就是图片的宽度与高度 this.Width = this.img.Width/2; this.Height = this.img.Height/2; } /// /// 创建一个方法,把自己绘制在游戏界面上面 /// /// gdi的画笔 public void Draw(Graphics g) { g.DrawImage(this.img, this.X, this.Y, this.Width, this.Height); } } } ``` * 在主窗体上面完成飞机的创建 ```csharp //窗体加载事件 private void MainForm_Load(object sender, EventArgs e) { DataUtil.hero = new Hero(150,500); //创建真正的飞机 } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.DrawImage(Properties.Resources.background,0,bgY); DataUtil.hero.Draw(e.Graphics); //画飞机 } ``` #### 鼠标控制玩家飞机 当玩家飞机创建好了以后,我们需要把玩家飞机的移动跟随鼠标移动 ```csharp //重写鼠标移动的方法 protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); //在这里,要控制玩家飞机的移动 改变飞机的横纵坐标就可以了 //鼠标的坐标就是玩家飞机的坐标 if (DataUtil.hero!=null) { DataUtil.hero.X = e.X; DataUtil.hero.Y = e.Y; } } ``` #### 隐藏鼠标光标 ```csharp //窗体加载事件 private void MainForm_Load(object sender, EventArgs e) { DataUtil.hero = new Hero(150,500); //创建真正的飞机 System.Windows.Forms.Cursor.Hide(); //隐藏光标 } ``` ### 创建数据管理对象 在我们这个游戏的项目里面,各位同学可以看到有很多个对象,例如玩家飞机,玩家子弹,敌人飞要, 道具等东西,这些东西都需要一个专门的对象来管理 现在我们就来创建一个数据管理对象,专门用来管理页面上面的数据对象 ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace whuc.PlaneGame { /// /// 该对象用于管理页面上面的数据 /// 它就相当于仓库,或叫军需库 /// public class DataUtil { /// /// 玩家飞机 /// public static Hero hero; } } ``` ### 玩家发射子弹 #### 创建子弹对象 ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; namespace whuc.PlaneGame { /// /// 玩家飞机的子弹 /// public class Bullet { public int X { get; set; } public int Y { get; set; } public Image img { get; set; } public int Width { get; set; } public int Height { get; set; } public Bullet(int x,int y) { this.img = Properties.Resources.bullet11; this.X = x; this.Y = y; this.Width = this.img.Width ; this.Height = this.img.Height ; } /// /// 子弹移动的方法 /// public void Move() { //玩家的子弹是向上运行的 所以它的纵坐标应该是减小的 this.Y = this.Y - 40; } /// /// 创建一个方法,把自己绘制在游戏界面上面 /// /// gdi的画笔 public void Draw(Graphics g) { //每次画自己之前,都要让自己移动一下 this.Move(); g.DrawImage(this.img, this.X, this.Y, this.Width, this.Height); } } } ``` > **代码说明** > > 1. 这个创建方式与之前飞机对象的创建非常相似 > > 2. 它多了一个Move的方法,那是因为玩家飞机是随着鼠标在移动,而子弹是自己在移动,所以我们有一个Move的方法 > > 3. 要弄清楚,子弹不可能是一个,它是多个,所以,我们要在`DataUtil`里面创建一修正集合 > > ```csharp > /// > /// 创建一个玩家飞机的子弹集合 > /// > public static List bulletList = new List(); > ``` > > 4. 弄清楚子弹与飞机的关系,是人在操作飞机,飞机在发身子弹,所以,飞机对象里面应该有一个发射子弹的方法,拉下来,请看下一章 #### 飞机发射子弹 > 首先我们要弄清楚一个概念,是谁在发身子弹? 经过分析得到,是飞机在发射子弹 在飞机对象下面添加一个发射的方法 **Hero.cs里面去添加如下方法** ```csharp /// /// 玩家飞机发射子弹的方式 /// public void Fire() { if (this.IsTowBullet) { //说明要发双排子弹 Bullet b_left = new Bullet(this.X, this.Y); Bullet b_right = new Bullet(this.X, this.Y); //修正左边子弹的位置 b_left.X = b_left.X + this.Width / 4 - b_left.Width / 2; //修正右边子弹的位置 b_right.X = b_right.X + this.Width / 4 * 3 - b_right.Width / 2; DataUtil.bulletList.Add(b_left); DataUtil.bulletList.Add(b_right); } else { //玩家飞机发射的子弹是有很多个的 Bullet b = new Bullet(this.X, this.Y); //修正子弹的坐标 b.X = b.X + this.Width / 2 - b.Width / 2; //用集全去装所有的玩家飞机子弹 DataUtil.bulletList.Add(b); } } ``` > **代码说明**: > > 1. 上面的代码是要加在Hero.js里面 > 2. 上面是把单排子弹与双排子弹都完成了,通过添加一个属性`IsTowBullet`来完成的,所以我们在上面定义属性的时候,还需添加如下属性 ```csharp public bool IsTowBullet { get; set; } //飞机是否是双排子弹 public Hero(int x,int y) { //这里还有之前的代码..... this.IsTowBullet = false; //默认情况下,它不是双排子弹 } ``` #### 游戏界面上面显示发射的子弹 1. 重新创建一个定时器,用于每隔一段时间发射子弹 ![1558247643098](assets/1558247643098.png) ![1558247753378](assets/1558247753378.png) 在上面的截图当, 我们把Interval设置为了250,这代表一秒钟(1000毫秒)发射4颗子弹 当定时器进行操作的时候,就发发射子弹 ```csharp //发射子弹的定时器执行事件 private void timer2_Tick(object sender, EventArgs e) { if (DataUtil.hero!=null) { DataUtil.hero.Fire(); //发射子弹 } } ``` > 在`MainForm`窗里的代码里面,添加如下代码,告诉程序,时间到了以后,发射子弹 2. 绘画子弹 ```csharp protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.DrawImage(Properties.Resources.background,0,bgY); //画背景 DataUtil.hero.Draw(e.Graphics); //画飞机 //画玩家子弹 foreach(Bullet b in DataUtil.bulletList) { b.Draw(e.Graphics); } } ``` ### 游戏对象的封装过程 在刚刚的代码里面,我们发现一个问题,就是有很多相同的代码 ,所以这个时候,我们希望把一些代码做到一个面向对象的封装 #### GameObject的封装 ```csharp using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; namespace whuc.PlaneGame { /// /// 游戏对象 抽象类,不能够直接实例化 /// public abstract class GameObject { public int X { get; set; } public int Y { get; set; } public Image img { get; set; } //如果要使用图片,那么,需要导命名空间System.Drawing; public int Width { get; set; } //飞机的宽度与高度 public int Height { get; set; } public GameObject(int x,int y,Image img) { this.X = x; this.Y = y; this.img = img; this.Width = this.img.Width; this.Height = this.img.Height; } /// /// 创建一个方法,把自己绘制在游戏界面上面 /// /// gdi的画笔 public virtual void Draw(Graphics g) { g.DrawImage(this.img, this.X, this.Y, this.Width, this.Height); } } } ``` #### Hero玩家飞机的封装 ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; namespace whuc.PlaneGame { /// /// 玩家飞机的对象 /// public class Hero:GameObject { public bool IsTowBullet { get; set; } //飞机是否是双排子弹 public Hero(int x,int y):base(x,y,Properties.Resources.hero1) { this.IsTowBullet = false; //默认情况下,它不是双排子弹 this.Width = this.img.Width / 2; this.Height = this.img.Height / 2; } /// /// 玩家飞机发射子弹的方式 /// public void Fire() { if (this.IsTowBullet) { //说明要发双排子弹 Bullet b_left = new Bullet(this.X, this.Y); Bullet b_right = new Bullet(this.X, this.Y); //修正左边子弹的位置 b_left.X = b_left.X + this.Width / 4 - b_left.Width / 2; //修正右边子弹的位置 b_right.X = b_right.X + this.Width / 4 * 3 - b_right.Width / 2; DataUtil.bulletList.Add(b_left); DataUtil.bulletList.Add(b_right); } else { //玩家飞机发射的子弹是有很多个的 Bullet b = new Bullet(this.X, this.Y); //修正子弹的坐标 b.X = b.X + this.Width / 2 - b.Width / 2; //用集全去装所有的玩家飞机子弹 DataUtil.bulletList.Add(b); } } } } ``` #### Bullet玩家飞机子弹的封装 ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Drawing; namespace whuc.PlaneGame { /// /// 玩家飞机的子弹 /// public class Bullet:GameObject { public Bullet(int x,int y):base(x,y,Properties.Resources.bullet11) { } /// /// 子弹移动的方法 /// public void Move() { //玩家的子弹是向上运行的 所以它的纵坐标应该是减小的 this.Y = this.Y - 40; } //但是继承下来的Draw不能够满足我们的方法 //在c#里面,所有的虚方法都可以被重写 public override void Draw(Graphics g) { this.Move(); base.Draw(g); //调父类方法 } } } ``` > 经过上面的的封装以后,我们的游戏大概的框架就出来了 > > **代码说明**: > > 1. `GameObject`为什么是抽象类是因为这个对象不能够直接被`new`出来,应该是一个很抽象的东西 > 2. `GameObject`里面的`Draw`方法为什么是一个`virtual` 的方法,那是因为这个方法在子类Bullet里面无法完成全部的功能 ,所以子类要去重写这个方法 ### 敌人飞机对象的创建 经过素材分析 ,我们发现敌人的飞机是有三种类型的,分别是小飞机,中飞机以及大飞机,所以在创建这个对象的时候有一点不同就是一个对象要同时描述三咱类型的飞机 敌人飞机的对象是`Enemy`对象,大致代码如下 ```csharp using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; namespace whuc.PlaneGame { /// /// 敌人的飞机 /// public class Enemy { public int X { get; set; } public int Y { get; set; } public Image img { get; set; } public int Width { get; set; } public int Height { get; set; } //还应该有一个特殊的属性叫type 0代表小飞机,1代表中飞机,2代表大飞机 public int Type { get; set; } public int Speed { get; set; } //飞机的移动速度 public Enemy(int x,int y,int type) { this.X = x; this.Y = y; this.Type = type; //飞机的图片它没有固定下来,它是根据我们飞机的类型来决定的 if (this.Type==0) { //小飞机 this.img = Properties.Resources.enemy0; this.Speed = 5; } else if (this.Type==1) { //中飞机 this.img = Properties.Resources.enemy1; this.Speed = 3; } else if (this.Type==2) { //大飞机 this.img = Properties.Resources.enemy2; this.Speed = 2; } this.Width = this.img.Width; this.Height = this.img.Height; } /// /// 敌人飞机移动的方法 /// public void Move() { this.Y = this.Y + this.Speed; //当飞机移动到屏幕外边的时候,应该让自己消失 if (this.Y>850) { //把自己移除掉 DataUtil.enemyList.Remove(this); } } //把自己画出来 public void Draw(Graphics g) { this.Move(); g.DrawImage(this.img, this.X, this.Y, this.Width, this.Height); } } } ``` ### 绘制敌人飞机 1. 敌人飞机与玩家子弹都有一个特点,它们都是多个,所以在这里, 我们需要定义一个集合来装这个飞机,这个时候,我们需要在`DataUtils`里面创建一个集合对象 ```csharp /// /// 创建玩家飞机的集合 /// public static List enemyList = new List(); ``` 2. 在界面上面添加飞机 在添加飞机的时候,要注意一件事情就是游戏在一个界面上面不能同时添加太多的飞要,也不能只添加一个类型的飞机,更不能添加太多的太飞机,所以这个时候,我们需要用到一个随机数概率的问题 ```csharp //闹钟响了以后要做什么 private void timer1_Tick(object sender, EventArgs e) { bgY = bgY + 5; if (bgY>0) { bgY = -850; } this.Invalidate(); //重新去绘画窗体 //判断一下当前游戏界面上面,敌人的飞机是否满足小于8架的条件 if (DataUtil.enemyList.Count<8) { //说明飞机不够 int count = 8 - DataUtil.enemyList.Count; Random rd = new Random(); //创建一个随机数对象 for (int i = 0; i < count; i++) { //出现飞机概率 小飞机 85% 中飞机 10% 大飞机5% int temp = rd.Next(100); //产生一个100以内的随机数 int type = 0; //默认是小飞机 if (temp>=95) { type = 2; } else if (temp >= 85) { type = 1; } else { type = 0; } Enemy enemy = new Enemy(rd.Next(480), 50, type); DataUtil.enemyList.Add(enemy); } } } ``` > **说明**:上面的代码里面,temp是一个随机数的问题,这个随机数有一个概率的控制 > > 同时在这里,我们也有一个飞机数量的控制,我们在添加飞机的时候,一直在判断这个飞机的数量是否小于8架 3. 在游戏界面上面画出敌机 ```csharp protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.DrawImage(Properties.Resources.background,0,bgY); //画背景 DataUtil.hero.Draw(e.Graphics); //画飞机 //画玩家子弹 for (int i = DataUtil.bulletList.Count - 1; i >= 0; i--) { DataUtil.bulletList[i].Draw(e.Graphics); } //画敌机 //foreach只适合于静态集合(集合不会发生改变,我们就用forEach) //当集合如果是动态集合的时候,我们一般都使用倒序 for (int i = DataUtil.enemyList.Count - 1; i >= 0; i--) { DataUtil.enemyList[i].Draw(e.Graphics); } } ``` ### 子弹与敌机的碰撞检测 在游戏过程当中,物体的碰撞我们可以理解为两个矩形发生相交过程,所以这个时候的碰撞检测其实就是我们的的矩形相交检测 #### 获取当前游戏对象的矩形 > 在`winForm`的基础里面,我们是有固定的方法来得到矩形的,特别是在`GDI`里面,可以直接调用方法来获取矩形,那么现在,我们就来实现这个功能 现在,我们分别在`GameObject`对象与`Enemy`对象里面去创建我们的如下方法 ```csharp /// /// 获取游戏对象矩形 /// /// 矩形 public Rectangle getRectangle() { return new Rectangle(this.X, this.Y, this.Width, this.Height); } ``` > **说明**:上面的方法里,我们是根据固定的*横纵坐标*来创建*固定大小*的矩形 #### 检测矩形相交 在碰撞检测的环节里面,其实应该有多种情况发生 * 玩家飞机的子弹与敌人飞机进行了碰撞 * 玩家飞机与敌人飞机发生了碰撞 * 玩家习机与道具发生了碰撞 现在在里面,我们就针对这三种情况来进行判断 ```csharp /// /// 检测两个物体是否有碰撞过程 /// public void checkCrash() { //在遍历的时候,一定要考虑一种情况,就是倒序遍历 //第一种情况:玩家飞机的子弹与敌人飞机进行了碰撞 for (int i = DataUtil.bulletList.Count - 1; i >= 0;i-- ) //子弹 { for (int j = DataUtil.enemyList.Count - 1; j >= 0;j-- ) //敌机 { //检测这两个对象的矩形是否有相交过程 Rectangle rect1 = DataUtil.bulletList[i].getRectangle(); Rectangle rect2 = DataUtil.enemyList[j].getRectangle(); if (rect1.IntersectsWith(rect2)) { //子弹把敌机打中了 DataUtil.bulletList.RemoveAt(i); //移除当前子弹 DataUtil.enemyList[j].IsDie(); //检测当前飞机是否死亡 //一颗子弹只可以使用一次,所以这颗子弹是肯定不能再和其它的敌机做碰撞检测的 break; } } } } ``` > 在不同的飞机下面,我们是有不同的生命值的,小飞机一条命,中飞机3条命,大飞机10条命,所以这个时候,我们需要在飞机对象里面,添加一个新的属性值`Life`用来描述当前飞机的生命值 ```csharp using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; namespace whuc.PlaneGame { /// /// 敌人的飞机 /// public class Enemy { public int X { get; set; } public int Y { get; set; } public Image img { get; set; } public int Width { get; set; } public int Height { get; set; } //还应该有一个特殊的属性叫type 0代表小飞机,1代表中飞机,2代表大飞机 public int Type { get; set; } public int Speed { get; set; } //飞机的移动速度 public int Life { get; set; } //飞机的生命值 public Enemy(int x,int y,int type) { this.X = x; this.Y = y; this.Type = type; //飞机的图片它没有固定下来,它是根据我们飞机的类型来决定的 if (this.Type==0) { //小飞机 this.img = Properties.Resources.enemy0; this.Speed = 5; this.Life = 1; } else if (this.Type==1) { //中飞机 this.img = Properties.Resources.enemy1; this.Speed = 3; this.Life = 3; } else if (this.Type==2) { //大飞机 this.img = Properties.Resources.enemy2; this.Speed = 2; this.Life = 10; } this.Width = this.img.Width; this.Height = this.img.Height; } /// /// 敌人飞机移动的方法 /// public void Move() { this.Y = this.Y + this.Speed; //当飞机移动到屏幕外边的时候,应该让自己消失 if (this.Y>850) { //把自己移除掉 DataUtil.enemyList.Remove(this); } } //把自己画出来 public void Draw(Graphics g) { this.Move(); g.DrawImage(this.img, this.X, this.Y, this.Width, this.Height); } /// /// 获取游戏对象的矩形 /// /// 矩形 public Rectangle getRectangle() { return new Rectangle(this.X,this.Y,this.Width,this.Height); } /// /// 是否死亡 /// /// public bool IsDie() { this.Life--; if (this.Life<=0) { //你死了 //播放死亡音乐 System.Media.SoundPlayer sound = new System.Media.SoundPlayer(Properties.Resources.enemy0_down1); sound.Play(); DataUtil.enemyList.Remove(this); //把自己移除 return true; } else { //说明你被打了,但是你是一个残血的状态 要更改飞机的图片 if (this.Type==1) { //说明是中飞机 this.img = Properties.Resources.enemy1_hit; } else if (this.Type==2) { //说明是大飞机 this.img = Properties.Resources.enemy2_hit; } return false; } } } } ``` > 在上面的代码里面,我们添加了一个Life的属性,同时在当前对象的构造方法里面,根据它的飞机类型Type来决定它的生命值,最后添加了一个方法IsDie来决定它是否死亡,以及死亡的要做的事情 ### 敌机爆炸效果 > 首先要弄清楚一个概念,敌机在哪里死亡,爆炸就在哪里产生 1. 创建敌机爆炸的对象 ```csharp using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; namespace whuc.PlaneGame { /// /// 爆炸动画的对象 /// public class Boom { //小飞机爆炸的图片数组 public Image[] imgs0 = { Properties.Resources.enemy0_down11, Properties.Resources.enemy0_down2, Properties.Resources.enemy0_down3, Properties.Resources.enemy0_down4 }; //中飞机爆炸的图片动画 public Image[] imgs1 = { Properties.Resources.enemy1_down11, Properties.Resources.enemy1_down2, Properties.Resources.enemy1_down3, Properties.Resources.enemy1_down4 }; //大飞机爆炸的图片数组 public Image[] imgs2 ={ Properties.Resources.enemy2_down11, Properties.Resources.enemy2_down2, Properties.Resources.enemy2_down3, Properties.Resources.enemy2_down4, Properties.Resources.enemy2_down5, Properties.Resources.enemy2_down6 }; /** * 敌机在哪里死亡,爆炸就在哪里产生 */ public Enemy enemyPlane { get; set; } public Image[] imgs { get; set; } public int Width { get; set; } public int Height { get; set; } public Boom(Enemy enemyPlane) { this.enemyPlane = enemyPlane; //图片根据飞机的类型确定以后 if (this.enemyPlane.Type==0) { this.imgs = this.imgs0; } else if (this.enemyPlane.Type==1) { this.imgs = this.imgs1; } else if(this.enemyPlane.Type==2) { this.imgs = this.imgs2; } this.Width = this.imgs[0].Width; this.Height = this.imgs[0].Height; } /// /// 绘画游戏对象 /// /// public void Draw(Graphics g) { for (int i = 0; i < this.imgs.Length; i++) { g.DrawImage(this.imgs[i],this.enemyPlane.X,this.enemyPlane.Y,this.Width,this.Height); } //当爆炸动画播放完成以后,就要移除自己 DataUtil.boomList.Remove(this); } } } ``` > * 爆炸它不可能是一张图片,所以它应该是多张图片的集合(数组) > * 爆炸是根据飞机的类型来决定图片的,所以这个地方会有3个数组分别是imgs0,imgs1,imgs2 2. 敌机死亡的时候应该有一个爆炸动画,所以我们找敌机死亡的代码 在`Enemy`这个对象里面,找到`IsDie()`这个方法 ```csharp /// /// 是否死亡 /// /// public bool IsDie() { this.Life--; if (this.Life<=0) { //你死了 //播放死亡音乐 System.Media.SoundPlayer sound = new System.Media.SoundPlayer(Properties.Resources.enemy0_down1); sound.Play(); //播放爆炸动画 Boom b = new Boom(this); DataUtil.boomList.Add(b); DataUtil.enemyList.Remove(this); //把自己移除 return true; } else { //说明你被打了,但是你是一个残血的状态 要更改飞机的图片 if (this.Type==1) { //说明是中飞机 this.img = Properties.Resources.enemy1_hit; } else if (this.Type==2) { //说明是大飞机 this.img = Properties.Resources.enemy2_hit; } return false; } } ``` > * 核心代码在创建爆炸动画的地方 > > * 爆炸不可能只有一次,所以它应该是有一个集合 > > ```csharp > /// > /// 创建爆炸动画的集合 > /// > public static List boomList = new List(); > ``` > > 我们要在`DataUtil`里面添加如上的代码 3. 在游戏界面上面把添加的所有爆炸动画给画出来 在`MainForm`的窗体的`OnPaint`来进行如下代码 ```csharp protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); //...... 这里是之前其它的代码 //画爆炸动画 for (int i = DataUtil.boomList.Count - 1; i >= 0;i-- ) { DataUtil.boomList[i].Draw(e.Graphics); } } ``` **注意事项**:在创建爆炸动画效果的时候,一定要注意,敌人是爆炸以后消失,而爆炸动画自己画完以后,也要消失 ### 玩家飞机与敌机的碰撞检测过程 下面的就是在`checkCrash`里面的代码,它是第二种情况,用于玩家飞机与敌人飞机的检测,我们分两种情况 1. 先停止游戏 2. 询问用户是否重新开始游戏 ```csharp //第二种情况:敌人飞机与玩家飞机的碰撞 for (int i = DataUtil.enemyList.Count - 1; i >= 0;i-- ) { Rectangle rect1 = DataUtil.enemyList[i].getRectangle(); //拿到敌机的矩形 Rectangle rect2 = DataUtil.hero.getRectangle(); //拿到玩家飞机的矩形 //检测两个矩形是否有相交 if (rect1.IntersectsWith(rect2)) { //游戏停止 this.timer1.Enabled = false; this.timer2.Enabled = false; System.Windows.Forms.Cursor.Show(); //重新显示鼠标的光标 DialogResult dr = MessageBox.Show("你挂了,是否重新开始游戏","提示",MessageBoxButtons.YesNo,MessageBoxIcon.Asterisk); if (dr==DialogResult.Yes) { //重新开始游戏 Application.Restart(); } else { //退出游戏 Application.Exit(); } } } ``` > 游戏停止的主要代码指的就是把两个定时器给停掉 ### 游戏计分功能 当玩家飞机干掉敌人飞机以后,这个时候,我们怎么样去统计玩家的得分 每个敌机在此处都有不同的得分,所以,我们分别义如下情况 | 飞机类型 | 得分 | | -------- | ---- | | 小飞机 | 10分 | | 中飞机 | 30分 | | 大飞机 | 50分 | 这个时候,我们在`Enemy`这个对象里来完成分数定义功能 ```csharp public int Score { get; set; } //当前的飞向值多少分 ``` 接下来,在构造方法里面我们进行如下的设置 ```csharp public Enemy(int x,int y,int type) { //飞机的图片它没有固定下来,它是根据我们飞机的类型来决定的 if (this.Type==0) { //小飞机 //......此处还有其它代码 ,具体参照项目 this.Score = 10; } else if (this.Type==1) { //中飞机 //......此处还有其它代码 ,具体参照项目 this.Score = 30; } else if (this.Type==2) { //大飞机 //......此处还有其它代码 ,具体参照项目 this.Score = 50; } } ``` 接下来,我们在`DataUtil`里面定义一个全局变量,用于保存当前游戏的得分 ```csharp /// /// 游戏得分 /// public static int Score { get; set; } ``` 接下来,回到`Enemy`这个敌机对象里面,当飞机死亡的时候,我们要开始计分了 ```csharp /// /// 是否死亡 /// /// public bool IsDie() { this.Life--; if (this.Life<=0) { //你死了 //播放死亡音乐 //计算得分 //.... 其它代码请参考项目 DataUtil.Score += this.Score; //在原来的得分之上,加上现在的得分 //.... 其它代码请参考项目 return true; } else { //.... 其它代码请参考项目 return false; } } ``` 最后在`MainForm`里面把整个总得分画出来 ```csharp protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // ....这里还有其它代码 ,具体参照项目 //画得分 e.Graphics.DrawString("得分:"+DataUtil.Score.ToString(), new Font("微软雅黑", 18), new SolidBrush(Color.Red),10,10); } ``` > `DarwString`的方法是绘制文字的方法,`Font`是字体对象 ### 游戏排行榜制作 我们希望将每个玩家的游戏得分保存下来,在这里,我们要将一些数据保存到自己的电脑上面,以文件的形式去保存,这个时候就会涉及到一个文件流对象序列化 **思考**:到底应该以什么格式来保存我们的数据,保存哪些数据? > 我们要保存当前的用户昵称,什么时候玩的游戏,游戏的得分是多少 #### 创建游戏保存的对象 ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace whuc.PlaneGame { /// /// 每一位玩家的记录信息 对象序列化 Serializable加了这个特性,那么这个对象就可以序列化 /// [Serializable] public class User { /// /// 当前玩家的昵称 /// public string userNickName { get; set; } /// /// 玩家什么时候玩的 /// public DateTime playTime { get; set; } /// /// 玩家的最高得分 /// public int Score { get; set; } public User(string userNickName,DateTime playTime,int Score) { this.userNickName = userNickName; this.playTime = playTime; this.Score = Score; } } } ``` #### 创建结果窗体 * 注意窗体的宽度是480*850 * 注意窗体不能够改变大小 * 注意在这里要去掉最大化的功能 * 注意在这里设置一个结果的背景图 ![1558766073851](assets/1558766073851.png) ```csharp using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.IO; //导命名空间 using System.Runtime.Serialization.Formatters.Binary; //二进制流格式化的命名空间 namespace whuc.PlaneGame { public partial class ResultForm : Form { public ResultForm() { InitializeComponent(); } private void ResultForm_Load(object sender, EventArgs e) { //怎么样保存结果,我们是把写在了一个文件里面 /* * 数据应该放在数据库里面,但是我们还可以放到一个文件里面 * 在这里,我们需要使用文件流来写入我们的数据 (输入流 InputStream,输出流OutputStream) */ string filePath = Application.StartupPath + @"\game.dat"; //首先判断一下,有没有这个文件 if (File.Exists(filePath)) { //如果有这个文件,那么,我就拿到这个文件,把这个文件转成对象 using (FileStream fs=new FileStream(filePath,FileMode.OpenOrCreate,FileAccess.ReadWrite)) { //将这个文件流转换成对象 BinaryFormatter bf = new BinaryFormatter(); object obj = bf.Deserialize(fs); List userList = obj as List; //委婉转换 //现在就得到一个集合,得到这个集合,我们还要判断一下,当前这个用户之前有没有玩过这个游戏 int index = userList.FindIndex(u => u.userNickName == DataUtil.userNickName); if (index==-1) { //说明没有找到 //再把你当前的信息存放进去 userList.Add(new User(DataUtil.userNickName, DateTime.Now, DataUtil.Score)); //再把这个集合序列化以后放到文件里面去 bf.Serialize(fs, userList); } else { //说明找到了,找到以后去判断一下,它的之前的分数和现在的分数怎么样 if (userList[index].Score>DataUtil.Score) { //说明之前的分数高一些 this.label1.Text = userList[index].Score.ToString(); } else { //说明这一次分数才是最高分 this.label1.Text = userList[index].Score.ToString(); userList[index].Score = DataUtil.Score; //重新设置最高分 userList[index].playTime = DateTime.Now; //换成当前时间 //再把这个集合序列化以后放到文件里面去 bf.Serialize(fs, userList); } } } } else { //要是这个文件不存在呢 using (FileStream fs=new FileStream(filePath,FileMode.OpenOrCreate,FileAccess.ReadWrite)) { //我要我要直接创建一个集合 List userList = new List(); //再把你当前的信息存放进去 userList.Add(new User(DataUtil.userNickName, DateTime.Now, DataUtil.Score)); //再把这个集合序列化以后放到文件里面去 BinaryFormatter bf = new BinaryFormatter(); bf.Serialize(fs,userList); } this.label1.Text = DataUtil.Score.ToString(); //设置最高得分 } this.label2.Text = DataUtil.Score.ToString(); //设置当前得分 } //重写窗体关闭的方法 protected override void OnFormClosed(FormClosedEventArgs e) { base.OnFormClosed(e); Application.Exit(); } } } ``` > 重写关闭的方法是因为之前的`MainForm`被隐藏了,现在当我们关闭游戏结果的窗体的时候,要让整个程序退出