原型模式,实际上是从原型实例复制克隆出新实例,而绝不是从类去实例化,这个过程的区别一定要搞清楚!OK,那开始我们的实战部分。
假设我们要做一个打飞机游戏,游戏设定位纵版移动,单打。
既然是单打,那我们的主角飞机当然只有一架,于是我们写一个单例模式,此处我们省略主角代码。那么敌机呢?当然有很多架了,好,为了说明问题我们去繁就简,先写一个敌机类。
1public class EnemyPlane {
2 private int x;//敌机横坐标
3 private int y = 0;//敌机纵坐标
4
5 public EnemyPlane(int x) {//构造器
6 this.x = x;
7 }
8
9 public int getX() {
10 return x;
11 }
12
13 public int getY() {
14 return y;
15 }
16
17 public void fly(){//让敌机飞
18 y++;//每调用一次,敌机飞行时纵坐标+1
19 }
20}
代码第5行,初始化只接收x坐标,因为敌机一开始是从顶部出来所以纵坐标y必然是0。此类只提供getter而没有setter,也就是说只能在初始化时确定敌机的横坐标x,后续是不需要更改坐标了,只要连续调用第17行的fly方法即可让飞机跟雨点一样往下砸。
好了,我们开始绘制敌机动画了,先实例化出50架吧。
1public class Client {
2 public static void main(String[] args) {
3 List<EnemyPlane> enemyPlanes = new ArrayList<EnemyPlane>();
4
5 for (int i = 0; i < 50; i++) {
6 //此处随机位置产生敌机
7 EnemyPlane ep = new EnemyPlane(new Random().nextInt(200));
8 enemyPlanes.add(ep);
9 }
10
11 }
12}
注意代码第7行,觉不觉得每个迭代都实例化new出一个对象存在性能问题呢?答案是肯定的,这个实例化的过程是得不偿失的,构造方法会被调用50次,cpu被极大浪费了,内存被极大浪费了,尤其对于游戏来说性能瓶颈绝对是大忌,这会造成用户体验问题,谁也不希望玩游戏会卡帧吧。
那到底什么时候去new?游戏场景初始化就new敌机(如以上代码)?这关会出现500个敌机那我们一次都new出来吧?浪费内存!那我们实时的去new,每到一个地方才new出来一个!浪费CPU!如果敌机线程过多造成CPU资源耗尽,每出一个敌机游戏会卡一下,试想一下这种极端情况下,游戏对象实例很多的话就是在作死。
解决方案到底是什么呢?好,原型模式Prototype!上代码!我们把上面的敌机类改造一下,让它支持原型拷贝。
1public class EnemyPlane implements Cloneable{//此处实现克隆接口
2 private int x;//敌机横坐标
3 private int y = 0;//敌机纵坐标
4
5 public EnemyPlane(int x) {//构造器
6 this.x = x;
7 }
8
9 public int getX() {
10 return x;
11 }
12
13 public int getY() {
14 return y;
15 }
16
17 public void fly(){//让敌机飞
18 y++;//每调用一次,敌机飞行时纵坐标+1
19 }
20
21 //此处开放setX,为了让克隆后的实例重新修改x坐标
22 public void setX(int x) {
23 this.x = x;
24 }
25
26 //为了保证飞机飞行的连贯性
27 //这里我们关闭setY方法,不支持随意更改Y纵坐标
28// public void setY(int y) {
29// this.y = y;
30// }
31
32 //重写克隆方法
33 @Override
34 public EnemyPlane clone() throws CloneNotSupportedException {
35 return (EnemyPlane)super.clone();
36 }
37}
注意看从第21行开始的修改,setX()方法为了保证克隆飞机的个性化,因为它们出现的位置是不同的。第34行的克隆方法重写我们调用了父类Object的克隆方法,这里JVM会进行内存操作直接拷贝原始数据流,简单粗暴,不会有其他更多的复杂操作(类加载,实例化,初始化等等),速度远远快于实例化操作。OK,我们看怎么克隆这些敌机,做一个造飞机的工厂吧。
1public class EnemyPlaneFactory {
2 //此处用痴汉模式造一个敌机原型
3 private static EnemyPlane protoType = new EnemyPlane(200);
4
5 //获取敌机克隆实例
6 public static EnemyPlane getInstance(int x){
7 EnemyPlane clone = protoType.clone();//复制原型机
8 clone.setX(x);//重新设置克隆机的x坐标
9 return clone;
10 }
11}
此处我们省去抓异常,随后的事情就非常简单了,我们只需要很简单地调用EnemyPlaneFactory.getInstance(int x)并声明x坐标位置,一架敌机很快地就做好了,并且我们保证是在敌机出现的时候再去克隆,确保不要一开局就全部克隆出来,如此一来,既保证了实时性节省了内存空间,又保证了敌机实例化的速度,游戏绝不会卡帧!至于此处代码中的懒汉原型还可以怎样优化那就要根据具体场景了,交给大家自由发挥吧,这里只说明主要问题。
最后,还要强调一点就是浅拷贝和深拷贝的问题。假如我们的敌机类里有一颗子弹bullet可以射击我们的主角,如下。
1public class EnemyPlane implements Cloneable{
2 private Bullet bullet = new Bullet();
3 private int x;//敌机横坐标
4 private int y = 0;//敌机纵坐标
5
6 //之后代码省略……
7}
我们都知道Java中的变量分为原始类型和引用类型,所谓浅拷贝只是拷贝原始类型的指,比如坐标x, y的指会被拷贝到克隆对象中,对于对象bullet也会被拷贝,但是请注意拷贝的只是地址而已,那么多个地址其实真正指向的对象还是同一个bullet。
由于我们调用父类Object的clone方法进行的是浅拷贝,所以此处的bullet并没有被克隆成功,比如我们每架敌机必须携带的子弹是不同的实例,那么我们就必须进行深拷贝,于是我们的代码就得做这样的改动。
1public class EnemyPlane implements Cloneable{
2 private Bullet bullet = new Bullet();
3
4 public void setBullet(Bullet bullet) {
5 this.bullet = bullet;
6 }
7
8 @Override
9 protected EnemyPlane clone() throws CloneNotSupportedException {
10 EnemyPlane clonePlane = (EnemyPlane) super.clone();//先克隆出敌机,其中子弹还未进行克隆。
11 clonePlane.setBullet(this.bullet.clone());//对子弹进行深拷贝
12 return clonePlane;
13 }
14
15 //之后代码省略……
16}
当然对于Bullet类也同样实现了克隆接口,相信大家都学会了举一反三。