命令模式--在魔兽世界中的运用
魔兽世界中的命令场景
笔者以前是个普通的魔兽世界玩家,每个魔兽世界玩家心中都比别人多一个世界。但同时笔者是一名程序员,经常又会在程序员的世界去思考游戏中各种场景是怎么实现的。今天心血来潮,准备使用“命令模式”为魔兽世界设计一套“技能释放”系统,包括:命令设计、宏命令、游戏外挂等具体实现过程。
在讲解命令模式之前,首先让我们来回味下魔兽世界中法师职业的技能:寒冰箭、火球术、奥术强化、气定神闲 等等,由于技能太多这里只列出4个技能。这些技能与普通游戏没什么两样,但魔兽世界的强大之处在于 可以让玩家根据自己的按键习惯,随意设置技能释放的快捷键。比如我个人的设置:数字键1--寒冰箭、数字键2--气定神闲、数字键3--奥术强化、数字键4--火球术等等。但另外一个玩家快捷键可能是:数字键1--寒冰箭、数字键2--气定神闲、字母键Q--奥术强化、字母键E--火球术等等。
这说明玩家的键盘和技能释放动作是完全解耦的,他们之间的桥梁就是每个技能释放动作都被封装为一个个命令,可以根据个人按键习惯随意设置。这其实就是命令模式的典型运用场景。今天我们就来自己设计下魔兽世界的“命令体系”,在此之前首先看下什么是命令模式。
命令模式介绍
命令模式的定义:将请求封装成对象,以便使用不同的请求、日志、队列等来参数化其他对象;命令模式也支持撤销操作。
以魔兽世界的“命令体系”为参照,定义中的“其他对象”就是键盘,“将请求封装成对象”这个对象就是命令对象,“请求”就是具体的技能释放动作。“命令对象”将“键盘”和“具体的技能释放动作”解耦。命令模式的类图如下:
如果把Client去掉,可以发现跟上一章讲的“适配器模式”类图是一样的,但二者的目的不一样导致最终的实现方式也不一样。命令模式的作用是把命令发出者和命令执行者的责任分开(解耦),而适配器模式的作用是转换接口。以魔兽世界的命令体系为例,命令把“键盘”和“具体的技能释放动作”分开,键盘不会去实现具体的技能。
魔兽世界的命令体系实现
在开始代码实现环节之前,首先以上述类图定义角色:
请求者角色:类图中的Invoker对应键盘类Keyboard;
命令接口角色:新建一个Command类与类图中的Command类对应;
具体的命令角色:新建一系列的命令实现类与类图中ConcreteCommand类对应:
接收着角色:新建一系列的“技能实现类”与类图中的Receiver对应。
下面是开始实现各个角色。
1、接受者角色
首先来看接收着角色,这里只模拟实现魔兽世界里法师技能列表中的4个技能:寒冰箭、气定神闲、奥术强化、火球术。
寒冰箭实现类Frostbolt,法师施放寒冰箭需要两秒的时间“吟唱咒语”,说得简单点就是“读条”,可以用一个线程来模拟。在释放技能“读条”期间,可以被打断(撤销)。具体实现如下:
/** * 寒冰箭操作类 * Created by gantianxing on 2017/11/6. */ public class Frostbolt { //释法线程 private Thread thread; //释放寒冰箭,具体实现 public void releaseSkill(){ if(this.getThread()!=null && this.getThread().isAlive()){ System.out.println("上一个释法正在进行中"); return; } //寒冰箭释放时长2秒 Thread thread = new Thread(new CastFrostbolt(2)); thread.start(); this.setThread(thread); } //打断 技能释放 public void cansel(){ if(this.getThread() !=null && this.getThread().isAlive()){ this.getThread().interrupt();//中断释法 }else { System.out.println("目前没有释放寒冰箭"); } } public Thread getThread() { return thread; } public void setThread(Thread thread) { this.thread = thread; } } /** * 模拟寒冰箭释放过程 * Created by gantianxing on 2017/11/6. */ public class CastFrostbolt implements Runnable{ private int time;//释法时长 public CastFrostbolt(int time) { this.time = time; } public void run() { System.out.println("+++开始释放寒冰箭+++"); System.out.println(time+"秒读条ing"); try { Thread.sleep(time*1000); System.out.println("+++完成释放寒冰箭+++"); System.out.println(" "); } catch (InterruptedException e) { //技能被取消 System.out.println("+++取消释放寒冰箭,读条结束+++"); System.out.println(" "); } } }
火球术实现类Fireball,法师施放火球术需要5秒的“读条”时间(记不太清了),也可以用一个线程来模拟。在释放技能“读条”期间,同样可以被打断(撤销)。具体实现与寒冰箭的实现类似,这里就不贴代码了,大家可以自行实现。
奥术强化实现类ArcanePower,该技能是瞬发技能,不需要读条,所有不会被打断(撤销)。实现比较简单:
public class ArcanePower { //释放奥术强化,具体实现 public void releaseSkill(){ System.out.println("释放:奥术强化,接下来30秒内的攻击 暴击率提高30%"); } }
气定神闲实现类PresenceOfMind,该技能同样是瞬发技能,不需要读条,所有也不会被打断(撤销)。该技能释放后的下一次“读条”技能 改为“瞬发”,所以需要记录状态:
public class PresenceOfMind { //是已经释放 气定神闲 private boolean isReleaseed = false; //释放气定神闲,具体实现 public void releaseSkill(){ this.setIsReleaseed(true); System.out.println("释放:气定神闲,接下来的一次读条技能变为瞬发"); } public boolean isReleaseed() { return isReleaseed; } public void setIsReleaseed(boolean isReleaseed) { this.isReleaseed = isReleaseed; } }
到这里,4个技能实现完毕。
2、命令接口角色
命令接口Command,只定义了一些统一的方法:
public interface Command { void execute();//执行命令 void undo();//撤销命令 }
3、具体的命令角色
具体命令角色实现了接口Command,是对上述4个具体的法师技能的封装。
寒冰箭命令类FrostboltCommand,封装寒冰箭技能施放:
public class FrostboltCommand implements Command { private Frostbolt frostbolt; public FrostboltCommand(Frostbolt frostbolt) { this.frostbolt = frostbolt; } public void execute() { frostbolt.releaseSkill(); } public void undo() { frostbolt.cansel(); } }
火球术命令类FireballCommand,与寒冰箭命令类 实现类似,这里就不贴出代码了。
奥术强化命令类ArcanePowerCommand,奥术强化技能cd时间为30秒,在技能cd期间无法使用该技能,这里使用一个线程模拟技能cd中,具体实现为:
public class ArcanePowerCommand implements Command { private static boolean cd = false;//技能是否进入cd private ArcanePower arcanePower; public ArcanePowerCommand(ArcanePower arcanePower) { this.arcanePower = arcanePower; } public void execute() { if(this.cd == false){ this.cd = true; arcanePower.releaseSkill(); //定时清除技能cd optCd(); }else { System.out.println("奥术强化技能cd中"); } } public void undo() { System.out.println("奥术强化 是瞬发技能,不能撤销"); } private void optCd(){ Thread thread = new Thread(new Runnable() { public void run() { try { Thread.sleep(30*1000);// 奥术强化cd时间 30秒 System.out.println("奥术强化cd结束"); ArcanePowerCommand.cd = false; } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } }
气定神闲命令类PresenceOfMindCommand,该技能与奥术强化类似,技能cd时间同样为30秒,实现方式与ArcanePowerCommand类型,这里就不再贴出代码。
另外还有一个空命令实现类NoCommand,什么都不做,用于赋值给键盘上没有设置快捷键的按键。具体实现如下:
public class NoCommand implements Command{ public void execute() { } public void undo() { } }
4、请求者角色
这里的请求者角色,就是键盘Keyboard 。键盘的实现 主要完成上述5个命令的绑定,具体实现如下:
public class Keyboard { Command[] commands; //对应键盘上的10个数字键 0-9 Command undoCommand; //撤销按键,对应键盘上的空格键 public Keyboard() { commands = new Command[10]; //游戏刚开始没有设置快捷键,键盘上的这10个按键默认是空命令 Command noCommand = new NoCommand(); for (int i=0;i<10;i++){ commands[i] = noCommand; } } //为键盘上的指定按键 绑定命令 public void setCommand(int keyNum,Command command){ commands[keyNum] = command; } public void pushKeyNum(int keyNum){ commands[keyNum].execute(); undoCommand = commands[keyNum];//记录最近一次操作 } public void pushSpacekey(){ if(undoCommand != null){ undoCommand.undo(); } } }
到这里魔兽世界的“命令体系”设计和开发完成,下面可以开始玩游戏了。
游戏时间
在游戏开始前,玩家还需要按照各自习惯绑定下技能“命令”(当然真实的游戏中有默认按键设置),然后按下键盘上的指定按键,就可以开始“打怪”了。
public class Main { public static void main(String[] args) throws Exception{ //Step1 初始化技能 ArcanePower arcanePower = new ArcanePower();//奥术强化 PresenceOfMind presenceOfMind = new PresenceOfMind();//气定神闲 Frostbolt frostbolt = new Frostbolt();//寒冰箭 Fireball fireball = new Fireball(presenceOfMind);//火球术 //step2 初始化命令,对4个技能进行封装 ArcanePowerCommand arcanePowerCommand = new ArcanePowerCommand(arcanePower); PresenceOfMindCommand presenceOfMindCommand = new PresenceOfMindCommand(presenceOfMind); FrostboltCommand frostboltCommand = new FrostboltCommand(frostbolt); FireballCommand fireballCommand = new FireballCommand(fireball); //step3 初始化键盘,并为键盘绑定命令,玩家可以根据自己的喜好调整 命令位置 Keyboard keyboard = new Keyboard(); keyboard.setCommand(1,frostboltCommand);//按键1设置为寒冰箭命令 keyboard.setCommand(2,presenceOfMindCommand);//按键2设置为气定神闲命令 keyboard.setCommand(3,arcanePowerCommand);//按键3设置为奥术强化命令 keyboard.setCommand(4,fireballCommand);//按键4设置为火球术命令 //准备完毕,目前你我们只设置了4个快捷键,其他的就交个玩家自己去设置吧 //开始打boss啦 // 按1键 释放寒冰箭 keyboard.pushKeyNum(1); Thread.sleep(1999);//寒冰箭释法时长2秒 keyboard.pushKeyNum(1);//上一个释放还没有完成,再次释法无效 Thread.sleep(500);//按下一个键 人的反应时间,0.5秒 //按4键 释放火球术 keyboard.pushKeyNum(4); Thread.sleep(5000);//火球术释法时长5秒 Thread.sleep(500);//按下一个键 人的反应时间,0.5秒 } }
执行main方法,打印信息如下:
+++开始释放寒冰箭+++ 2秒读条ing 上一个释法正在进行中 +++完成释放寒冰箭+++ ---开始释放火球术--- 5秒读条ing ---完成火球术释放---
这次模拟分别施放了两次寒冰箭和一次次火球术,但有由于第一次寒冰箭,还没有施放结束,第二次施放没有成功。
火球术的伤害被寒冰箭高,但施法时间太长(读条),一般不会直接使用。但我们技能栏里,还有“气定神闲”技能。先施放“气定神闲”,再施法“火球术”就是瞬发(不用读条了),再加上“奥术强化”就更配了,这就是传说中的“气定 奥强 大火球”。具体操作如下(代码加在main方法后面):
//按1键 再次释放寒冰箭 keyboard.pushKeyNum(1); //0.5秒后 气定神闲和奥术强化cd时间到 Thread.sleep(500); keyboard.pushSpacekey();//取消寒冰箭 Thread.sleep(500); keyboard.pushKeyNum(2);//释放气定神闲 Thread.sleep(500); keyboard.pushKeyNum(3);//释放奥术强化 Thread.sleep(500); keyboard.pushKeyNum(4);//瞬发大火球 Thread.sleep(500); keyboard.pushKeyNum(2);//想再释放“气定神闲”还得等30秒cd
执行mian方法,打印信息如下:
+++开始释放寒冰箭+++ 2秒读条ing +++取消释放寒冰箭,读条结束+++ 释放:气定神闲,接下来的一次读条技能变为瞬发 释放:奥术强化,接下来30秒内的攻击 暴击率提高30% 瞬发火球术完成 气定神闲技能cd中 气定神闲cd结束 奥术强化cd结束
玩家在释放“寒冰箭”读条的过程中发现,气定神闲的cd结束,立即取消“寒冰箭”技能,开始释放“气定 奥强 大火球”组合技能。
这时玩家的操作顺序是,先按空格键 再依次按下2、3、4键。一次操作需要按4个键,对玩家手速是个巨大的考验,怎么办?别紧张“命令模式”中还有“宏命令”:
宏命令
所谓宏命令,就是把多个具体的动作放在一个命令中,动作发起者只需一个操作,就可以引发多个“接收者角色”执行多个动作。
在魔兽世界的场景中,就是玩家只需按下一个键,就可以完成“气定 奥强 大火球”的技能释放。下面来看宏命令的实现:
public class MacroCommand implements Command{ List<Command> commands = new ArrayList<Command>(); //添加命令 public void add(Command command){ commands.add(command); } //移除命令 public void remove(Command command){ commands.remove(command); } //批量执行命令 public void execute() { for(Command cmd : commands){ cmd.execute(); } } public void undo() { System.out.println("宏命令 暂不提供撤销功能"); } }
在main方法中,初始化宏命令,并放绑定到“数字键5”:
//宏命令初始化 MacroCommand macroCommand = new MacroCommand(); macroCommand.add(presenceOfMindCommand);//气定 macroCommand.add(arcanePowerCommand);//奥强 macroCommand.add(fireballCommand);//瞬发大火球 keyboard.setCommand(5,macroCommand);
气定、奥强的cd结束,玩家立即取消当前施法,再按下数字键5:
//按1键 再次释放寒冰箭 keyboard.pushKeyNum(1); //0.5秒后 气定神闲和奥术强化cd时间到 Thread.sleep(500); keyboard.pushSpacekey();//取消寒冰箭 keyboard.pushKeyNum(5);
执行mian方法,打印结果为:
+++开始释放寒冰箭+++ 2秒读条ing +++取消释放寒冰箭,读条结束+++ 释放:气定神闲,接下来的一次读条技能变为瞬发 释放:奥术强化,接下来30秒内的攻击 暴击率提高30% 瞬发火球术完成
游戏外挂
打boss的时候 一打就是好几分钟,一直不停的按键是件很累的事情。别紧张,我们可以开发一个游戏外挂(不影响游戏平衡的外挂)。针对某个boss的外挂需求是这样的:我们可以一直释放“寒冰箭”,有“气定”+“奥强”cd就使用瞬发“火球术”,在释放“寒冰箭”期间夹杂一些“火球术”,可以加深伤害。我们的外挂实现就是这样的:
int boss_blood = 10000;//boss初始血量 System.out.println("战斗开始"); //一次循环大约31秒 while (true){ //气定 奥强 cd结束,就使用宏命令"气定 奥强 大火球" keyboard.pushKeyNum(5); //一次循环 15.5秒,两次31秒。气定和奥强cd时间都是30秒 for(int j=0;j<2;j++){ for (int i = 0;i<4;i++){//4次寒冰箭 10秒 keyboard.pushKeyNum(1); Thread.sleep(2000);//寒冰箭释法时长2秒 Thread.sleep(500);//按下一个键 人的反应时间,0.5秒 } keyboard.pushKeyNum(4);//火球术释法时长5秒 Thread.sleep(5000);//寒冰箭释法时长2秒 Thread.sleep(500);//按下一个键 人的反应时间,0.5秒 } boss_blood = boss_blood - 2000; if(boss_blood <0){ System.out.println("boss被消灭,开始分装备啦"); break; } } }
执行main方法,打印信息如下:
战斗开始 释放:气定神闲,接下来的一次读条技能变为瞬发 释放:奥术强化,接下来30秒内的攻击 暴击率提高30% 瞬发火球术完成 +++开始释放寒冰箭+++ 2秒读条ing +++完成释放寒冰箭+++ //此处省略约3分钟的战斗画面 ---开始释放火球术--- 5秒读条ing 气定神闲cd结束 奥术强化cd结束 ---完成火球术释放--- boss被消灭,开始分装备啦
整个过程由外挂执行,玩家可以上个厕所休息下,3分钟后回来“分装备”就可以啦。
小结
各大游戏里的“命令体系”设计,就是“命令模式”的典型应用场景之一。学会了魔兽世界的“命令体系”设计,相信你也可以设计“英雄联盟”的命令了。
结合命令模式中的undo(撤销)操作,此模式还可以广泛用于其他场景:日志记录和恢复、以及事务处理等。具体就是在执行execute方法时 记录操作顺序和日志,在出现问题后,执行undo操作进行恢复。这里就不再展开讲解,可以自行实现。
本次总结 内容涉及太多魔兽世界游戏元素,对于魔兽世界玩家是一个福利。但对于没有玩过魔兽世界的朋友,要说声抱歉了。
出处:
http://moon-walker.iteye.com/blog/2398844
推荐阅读