前言
本科阶段写过一个小游戏,是一门课的大作业,当时能力有限,代码能力以及对设计模式的理解和运用都不够,后来研究生期间,由于《软件结构设计与模式分析》这门课的期末考试需要我们编写并分析一个软件,软件类型不限,由于觉得这款小游戏题材不错,又有趣味性,所以借鉴了该游戏的思路并对它进行了重构,不仅界面进行了大量优化,同时也加入了一些设计模式,大大提高了软件的扩展性,这里结合这个小游戏,分析几个游戏中使用到的设计模式: 单例模式,策略模式和工厂方法模式。
博客中没有对整个游戏的设计做详细的介绍,只是借这个游戏分析一下设计模式,游戏使用C#实现,游戏的VS工程和相关资料我上传到CSDN上了点击下载,使用VS2013打开就可以直接运行。大家可以结合代码看本文,这样能够更好的理解这个游戏和游戏中的几个设计模式。
文章目录
游戏简介
简介
游戏是一个非常简单的RPG小游戏,游戏中主要有两类角色,分别为Hero-英雄和Enemy-敌人(怪兽),英雄是由玩家控制的角色,怪兽是系统控制的角色,其中怪兽分为不同等级,有小怪和大怪,游戏内容比较简单,就是双方发射子弹攻击对方,如果怪兽将英雄的生命值打为0,游戏结束,如果英雄将最后的大怪生命值打为0,游戏胜利。
界面演示
本来想做成GIF动画演示的,但是由于GIF文件比较大,上传不了,这里贴张图片,展示一下游戏的界面,大家可以下载源码运行,就可以看到整个游戏运行过程了。
操作说明:"X"键发射子弹,方向键控制人物的移动
游戏整体结构
打开VS工程,打开其中的类图文件ClassDiagram1.cd,就可以看到整个游戏的类图了
游戏的类图如下
简单分析一下游戏的结构
- Element:所有角色的根类
- RoAndMi:继承自Element,是角色和子弹的基类
- Roles及其子类:游戏中的所有角色
- Missiles及其子类:游戏中所有角色的子弹
- FireBehavior及其子类:游戏中所有角色的发射子弹的行为
- HitCheck:游戏的主控类,用来控制游戏中所有元素
游戏详细的实现过程,读者可以看源码,结合类图看源码,相信读者很快就能非常清楚整个游戏了
下面的三个部分是游戏的核心
- Roles及其子类:游戏中的所有角色
- FireBehavior及其子类:游戏中所有角色的发射子弹的行为
- HitCheck:游戏的主控类,用来控制游戏中所有元素
分析游戏的时候,要把握好这三块。
下面我们就结合这个小游戏,分析三种设计模式:单例模式,策略模式和工厂方法模式。
单例模式
定义
确保一个类只有一个实例,并提供一个全局访问点。[1]P177(表示在参考文献[1]的177页,下同)
经典的单例模式实现
public class Singleton {
private static Singleton uniqueInstance;
// other useful instance variables here
private Singleton() {}
public static Singleton GetInstance()
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// other useful methods here
}
总结一下单例的实现就是:一个私有,两个静态
- 一个私有
就是私有构造函数
单例模式的思想就是一个类只有一个实例,即外部任何类都不能实例化该类,那么什么样的类外部不能实例化呢?我们知道,实例化一个类的时候,需要调用构造函数,而一般构造函数都是public的,所以能够被外部调用,所以能够在外部实例化,当将构造函数设置为private时,外部就不能调用类的构造函数了,也就不能实例化该类了,该类只能在类的内部实例化。这个思想是实现单例模式的关键。 - 两个静态:
1.静态成员变量uniqueInstance,该成员变量就是类的唯一实例
2.静态方法GetInstance(),用来获取该类的唯一实例
前面提到了使用私有构造函数是实现单例模式的关键,那么下面的问题就是怎么在外部获取该单例呢?由于任何外部类都不能实例化该类,所以我们无法通过使用类的对象来调用类里面的方法获取单例(即不能通过Singleton singleton=new Singleton();singleton.GetInstance()
来获取单例),只能通过类里面的静态方法,通过类名调用静态方法(Singleton.GetInstance()
)来获取单例,而静态方法只能调用静态成员,所以类的成员变量也必须是静态的。
适用性
当一个类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。[2]P84
对有些类来说,只有一个实例很重要,如线程池,注册表,文件系统等
虽然全局变量也可以提供全局访问点,但是不能防止你实例化多个对象
游戏中的实现
类图文件中双击HitCheck类,就能看到代码,当然也可以在工程中直接打开HitCheck.cs
/// <summary>
/// 主控类,负责游戏中的各种角色的管理
/// 1.AddElement()---添加元素
/// 2.RemoveElement()----删除元素
/// 3.Draw()----元素的绘制
/// 4.DoHitCheck()---元素之间的碰撞检测
/// 5.Restart()---重新开始游戏
/// </summary>
public class HitCheck
{
//游戏中的角色
private Hero myHero = null;
private List<MissileHero> missileHero = new List<MissileHero>();
private List<Roles> enemy = new List<Roles>();
private List<Missiles> enemyMissile = new List<Missiles>();
private List<Element> bombs = new List<Element>();
/// <summary>
/// 构造函数私有化,禁止在其他地方实例化
/// </summary>
private HitCheck() { }
private static HitCheck instance;
public static HitCheck GetInstance()
{
if (instance == null)
{
instance = new HitCheck();
}
return instance;
}
...
}
这个代码看上去是不是很熟悉,这就是个典型的单例模式的实现:一个私有,两个静态.
为什么要使用单例模式
刚开始写游戏的时候是没有用的,慢慢发现,游戏中的角色一旦过多,角色就很难管理,如角色的产生,角色的死亡,包括角色之间的碰撞检测。一旦游戏中要增加角色需要修改的代码很多,维护量比较大,所以就想设计一个类,实现对游戏中所有角色的管理,这样就可以很方便的对游戏中的角色进行管理。这个类主要控制游戏中的所有角色,包括对所有元素的增加,删除,以及碰撞检测(如英雄是否被敌人的子弹打中),这就要求该类只能有一个实例,不能有多个实例。不然游戏就会出错,所以设计为单例。读者分析一下HitCheck的源码就非常清楚其中使用单例的原因了。
多线程问题
经过上面的介绍和分析,读者对基本单例模式的实现和原理应该比较清楚了,那么是否这样的单例模式就非常好了呢?下面我们讨论一下在多线程中的问题。
还是看上面经典单例模式的代码
public class Singleton {
private static Singleton uniqueInstance;
// other useful instance variables here
private Singleton() {}
public static Singleton GetInstance()
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// other useful methods here
}
假设现在有两个线程,以下是他们的执行步骤:
多线程中,由于每个线程执行的顺序不确定,就有可能产生2个实例。
那怎么解决呢?这里提供以下两种方式,有更好的方式,欢迎大家提出来。
方法1:”急切”实例化
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton GetInstance() {
return uniqueInstance;
}
}
代码中,当类被加载时,静态变量uniqueInstance 会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。多线程的时候,由于类加载的时候就创建了实例,所以不会出现多个实例的情况。
方法2:“双重检查加锁”
class Singleton
{
private static volatile Singleton instance = null;
//程序运行时创建一个静态只读的辅助对象
private static readonly object syncObject= new object();
private Singleton() { }
public static Singleton GetInstance()
{
//第一重判断,先判断实例是否存在,不存在再加锁处理
if (instance == null)
{
//临界区!
//加锁的程序在某一时刻只允许一个线程访问
lock(syncObject)
{
//第二重判断
if(instance==null)
{
instance = new Singleton(); //创建单例实例
}
}
}
return instance;
}
}
为了更好地对单例对象的创建进行控制,此处使用了一种被称之为双重检查加锁机制。在双重检查锁定中,当实例不存在且同时有两个线程调用GetInstance()方法时,它们都可以通过第一重instancenull判断,然后由于lock锁定机制,只有一个线程进入lock中执行创建代码,另一个线程处于排队等待状态,必须等待第一个线程执行完毕后才可以进入lock锁定的代码,如果此时不进行第二重instancenull判断,第二个线程并不知道实例已经创建,将继续创建新的实例,还是会产生多个单例对象,因此需要进行双重检查。
volatile关键字
volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。当成员变量发生变化时,强迫线程将变化值回写到共享内存(线程共享进程的内存)。这样,读取这个变量的值时候每次都是从momery里面读取而不是从cache读,这样做是为了保证读取该变量的信息都是最新的,而无论其他线程如何更新这个变量。
此外,由于使用volatile关键字屏蔽掉了一些必要的代码优化,所以在效率上比较低,因此需要慎重使用。
如果没有volatile关键字,第二个线程就可能没有及时读到最新的值,比如进程2在第二重判断的时候,进程1已经产生了一个实例,但是进程2没有读到最新的值,读到的instance还是为null,那么就会产生多个实例了,那么即使使用了双重检查加锁,也有可能产生多个实例。
这两种方式在[1]P180~P182的处理多线程问题中也有非常清楚的阐述,用java描述,深入浅出,讲解地非常好。
两种方式的比较
”急切”实例化在类被加载时就将自己实例化,它的优点在于无须考虑多个线程同时访问的问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于“双重检查加锁”。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,”急切”实例化单例不及“双重检查加锁”单例,而且在系统加载时由于需要创建”急切”实例化单例对象,加载时间可能会比较长。
“双重检查加锁”单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题。
结束语
游戏中还有两个模式:策略模式和工厂方法模式在下面的博客中讲述,读者可以先看看下载下来的资料中的PPT的相关内容,结合类图,可以先自行分析游戏源码中的这两个模式。第一次使用Markdown写博客,虽然不太熟练,但是觉得Markdown还是很强大的,写出的博客更美观。
这里顺便推荐大家一本书:《Head First设计模式》,该本书获2005年第15届Jolt大奖,Jolt大奖是软件行业的"奥斯卡"奖。本书中的每个设计模式都结合具体实例,深入浅出,个人觉得比GOF设计模式更加通俗易懂。
该书的所有代码我上传到CSDN上了,结合书中的代码看这本书会更好。
点击下载
参考文献
[1] 《Head First设计模式(中文版)》 ,中国电力出版社
[2] 《设计模式:可复用面向对象软件的基础》(著名的GOF设计模式),机械工业出版社
非常感谢您的阅读,如果您觉得这篇文章对您有帮助,欢迎扫码进行赞赏。