第一章-宝箱抽奖模块与代码设计(一)
简要 | 信息 |
---|---|
作者 | 卡卡 |
博客 | http://blog.csdn.net/kakashi8841 |
邮箱 | john.cha@qq.com |
本文所属专栏 | http://blog.csdn.net/column/details/12687.html |
无聊的开场白
每篇文章的背后都有个”高大上”的故事
大家好,我是卡卡(取自火影忍者中卡卡西)。其实这篇文章是快到截止日期才写的,因为自己创业中,然后最近刚好有个同学创业,很多技术上的东西需要我帮忙,因此本来和CSDN约好的这篇文章也是一拖再拖。然后在快到文章截止日期的某个晚上,有个朋友刚好和我聊起怎样提升代码质量的问题。于是就有了这篇文章。
怎样的代码才算是好的代码
Linux大神大概是这么说的,两个程序员写出的代码不同在于他们的对程序的品味不同。确实,有的人对代码比较敏感,一眼就能看出,这里写的不好,这里不灵活,这里可能有问题。
那么很多积极上进的少年肯定还是希望能提高自己代码设计的质量。本文将通过一个实例来说明怎样逐步优化代码。
简单的需求
很多游戏中都有开宝箱的功能。比如现在你收到一个任务。让你去做一个开宝箱的模块。需求如下:
1. 玩家可以看到三种白银、黄金、钻石三种类型的宝箱,分别消耗游戏中的白银、黄金和钻石。
2. 这三种宝箱玩家可以请求开1次和开10次。
简单的代码(Java实现)
一些基本的类
玩家类
存放玩家资源和其他信息的类,这里为了演示,去掉无关的属性,只有资源属性。
这个类目前的写法是比较常见的写法,但是这个类有很多优化的地方。文章后面会说。
package com.kakashi01.player.domain;
public class Player {
private static final int MAX_SLIVER = Integer.MAX_VALUE;
private static final int MAX_GOLD = Integer.MAX_VALUE;
private static final int MAX_DIAMOND = Integer.MAX_VALUE;
private int id;
private int sliver; // 白银
private int gold; // 黄金
private int diamond; // 钻石
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getSliver() {
return sliver;
}
public void setSliver(int sliver) {
this.sliver = sliver;
}
public int getGold() {
return gold;
}
public void setGold(int gold) {
this.gold = gold;
}
public int getDiamond() {
return diamond;
}
public void setDiamond(int diamond) {
this.diamond = diamond;
}
/**
* 修改白银
*
* @param alter
* 修改值。正数表示增加,负数表示减少
* @return
*/
public boolean alterSliver(int alter) {
int old = sliver;
sliver = alter(sliver, alter, 0, MAX_SLIVER);
return old != sliver;
}
/**
* 修改钻石
*
* @param alter
* 修改值。正数表示增加,负数表示减少
* @return
*/
public boolean alterDiamond(int alter) {
int old = diamond;
diamond = alter(diamond, alter, 0, MAX_DIAMOND);
return old != diamond;
}
/**
* 修改黄金
*
* @param alter
* 修改值。正数表示增加,负数表示减少
* @return
*/
public boolean alterGold(int alter) {
int old = gold;
gold = alter(gold, alter, 0, MAX_GOLD);
return old != gold;
}
/**
* 修改传入的current,修改量为alter。修改后应该在[min, max]范围内。
*
* @param current
* 修改前的值
* @param alter
* 修改量
* @param min
* 下限(包含)
* @param max
* 上限(包含)
* @return 返回修改后的值
*/
private int alter(int current, int alter, int min, int max) {
if (alter > 0) {
current += alter;
if (current < min || current > max) {
current = max;
}
} else if (alter < 0) {
if (current >= -alter) {
current += alter;
}
if (current < min) {
current = min;
}
}
return current;
}
}
抽奖服务类
这个类暂时只有一个空方法。这也是我们下面将实现的内容。
package com.kakashi01.lottery;
public class LotteryService {
/**
* 抽奖方法
* @param lotteryType 宝箱类型
* @param timesType 次数类型
*/
public void lottery(int lotteryType, int timesType){
}
}
我们需要定义一个用于表示抽奖信息的类ConfigLottery。根据宝箱类型和次数类型,就可以取到抽奖信息。然后进行抽奖。
因此在LotteryService中增加一个lotteryMap(Map类型),以及getConfigLottery方法用于从lotteryType和timesType映射到ConfigLottery。
private final Map<Integer, Map<Integer, ConfigLottery>> lotteryMap = new HashMap<>();
public ConfigLottery getConfigLottery(int lotteryType, int timesType) {
Map<Integer, ConfigLottery> map = lotteryMap.get(lotteryType);
if (map == null) {
return null;
}
return map.get(timesType);
}
先大致确定抽奖方法lottery的逻辑,确定后的LotteryService代码如下:
package com.kakashi01.lottery;
import java.util.HashMap;
import java.util.Map;
import com.kakashi01.lottery.domain.ConfigLottery;
import com.kakashi01.player.domain.Player;
public class LotteryService {
private final Map<Integer, Map<Integer, ConfigLottery>> lotteryMap = new HashMap<>();
public ConfigLottery getConfigLottery(int lotteryType, int timesType) {
Map<Integer, ConfigLottery> map = lotteryMap.get(lotteryType);
if (map == null) {
return null;
}
return map.get(timesType);
}
/**
* @param player
* 进行抽奖的玩家
* @param lotteryType
* 宝箱类型
* @param timesType
* 次数类型
*/
public void lottery(Player player, int lotteryType, int timesType) {
ConfigLottery configLottery = getConfigLottery(lotteryType, timesType);
if (configLottery == null) {
System.err.println("Can not found such ConfigLottery : " + lotteryType + ", " + timesType);
return;
}
if (tryCostResource(player, configLottery)) {
dropItem(configLottery);
} else {
System.err.println("Not enough resource for lottery : " + lotteryType + ", " + timesType);
return;
}
}
private void dropItem(ConfigLottery configLottery) {
// TODO 掉落物品
}
private boolean tryCostResource(Player player, ConfigLottery configLottery) {
// TODO 扣除资源
return false;
}
}
可以看到LotteryService中lottery的方法签名变了,增加了Player对象,而且还增加了存根方法dropItem和tryCostResource。这两个方法都被lottery调用。
有了上面大致的逻辑流程。我们就可以一步步来实现功能了。
扣除资源方法tryCostResource,应该怎么实现?
大家应该很容易就想到了,既然已经取到了抽奖的配置ConfigLottery,那么只要把这个抽奖需要多少资源保存在ConfigLottery中的一个字段就可以了。比如ConfigLottery中增加一个cost字段代表消耗多少资源,当lotteryTyoe为1、2、3时分别代表白银宝箱、黄金宝箱、钻石宝箱,那么自然可以根据lotteryType的值判断需要消耗什么资源。
那么,此时tryCostResource的实现代码为:
private boolean tryCostResource(Player player, ConfigLottery configLottery) {
switch (configLottery.getLotteryType()) {
case ConfigLottery.SLIVER:
return player.alterSliver(-configLottery.getCost());
case ConfigLottery.GOLD:
return player.alterGold(-configLottery.getCost());
case ConfigLottery.DIAMOND:
return player.alterDiamond(-configLottery.getCost());
}
return false;
}
很直观,根据不同的宝箱类型。然后扣除不同的资源。并返回是否扣除成功。
对了忘记说,ConfigLottery的代码已经被改为:
package com.kakashi01.lottery.domain;
public class ConfigLottery {
public static final int SLIVER = 1;
public static final int GOLD = 2;
public static final int DIAMOND = 3;
private int lotteryType;
private int timesType;
private int cost; // 抽奖需要消耗的资源
public int getLotteryType() {
return lotteryType;
}
public void setLotteryType(int lotteryType) {
this.lotteryType = lotteryType;
}
public int getTimesType() {
return timesType;
}
public void setTimesType(int timesType) {
this.timesType = timesType;
}
public int getCost() {
return cost;
}
public void setCost(int cost) {
this.cost = cost;
}
}
简单,粗暴,有效。然而这么简单的代码,也是存在一些可以优化的地方。我们文章后面再说。现在还是先赶紧实现需求。只剩下dropItem方法写完就可以下班了~
dropItem与大转盘
抽奖,开宝箱其实很像平常见到的转盘,转盘上画满各种奖品,然后旋转转盘,等待转盘停下时,指针指向的物品就是奖品。那么,应该怎样用代码描述这样的一个行为。其实,根据数学的一些基础知识,我们知道,转盘为360度。如果某个物品占据了一个80度的扇形,那么意味着这个物品被抽中的概率为80/360。而转盘上所有扇形加起来的角度之和为360度,即为概率的基数。同样的道理,我们只要有以下数据,就可以模拟转盘进行随机抽取。
我们需要该宝箱会掉落什么物品,多少数量,以及每个物品所占的比重。那么修改ConfigLottery为如下代码:
package com.kakashi01.lottery.domain;
import java.util.List;
public class ConfigLottery {
public static final int SLIVER = 1;
public static final int GOLD = 2;
public static final int DIAMOND = 3;
private int lotteryType;
private int timesType;
private int cost; // 抽奖需要消耗的资源
private List<ConfigLotteryItem> items; // 掉落的物品
public int getLotteryType() {
return lotteryType;
}
public void setLotteryType(int lotteryType) {
this.lotteryType = lotteryType;
}
public int getTimesType() {
return timesType;
}
public void setTimesType(int timesType) {
this.timesType = timesType;
}
public int getCost() {
return cost;
}
public void setCost(int cost) {
this.cost = cost;
}
public List<ConfigLotteryItem> getItems() {
return items;
}
public void setItems(List<ConfigLotteryItem> items) {
this.items = items;
}
}
可以看到相对之前只是增加了items字段。ConfigLotteryItem的代码如下:
package com.kakashi01.lottery.domain;
public class ConfigLotteryItem {
private int modelID; // 物品模型ID
private int num; // 物品数量
private int weight; // 权重
public int getModelID() {
return modelID;
}
public void setModelID(int modelID) {
this.modelID = modelID;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
可以看到ConfigLotteryItem的代码很简单,它只记录了掉落的物品模型ID,掉落的物品数量,该物品的权重。
那么,这时候就可以使用该信息来掉落物品了。修改LotteryService中dropItem的代码如下:
private void dropItem(ConfigLottery configLottery) {
Random rand = new Random();
for (int i = 0; i < configLottery.getTimesType(); i++) {
List<ConfigLotteryItem> items = configLottery.getItems();
int totalWeight = 0;
for (ConfigLotteryItem item : items) {
totalWeight += item.getWeight();
}
int randNum = rand.nextInt(totalWeight);
for (ConfigLotteryItem item : items) {
if (randNum < item.getWeight()) {
System.out.println("Drop item " + item.getModelID() + ", " + item.getNum());
break;
}
randNum -= item.getWeight();
}
}
}
这里面先计算这个抽奖信息的总权重totalWeight。策划的配置不一定使得所有物品的权重之和为100,更不一定为360。权重更多的表示的是该物品在整个转盘中所占的比例,是相对其他物品而言。因此,需要把所有物品的权重进行求和。
然后,在[0, totalWeight)区间产生随机数。你可以理解为在圆盘中的0~360之间选择一个角度。然后第二层循环则是判断该随机数落在哪个区间。落在这个区间,则表示掉落这个物品。
接下来编写测试代码观察运行结果:
package com.kakashi01.lottery;
import java.util.LinkedList;
import java.util.List;
import com.kakashi01.lottery.domain.ConfigLottery;
import com.kakashi01.lottery.domain.ConfigLotteryItem;
import com.kakashi01.player.domain.Player;
public class LotteryDemo {
private static final int[] lotteryTypes = new int[] { ConfigLottery.SLIVER, ConfigLottery.GOLD,
ConfigLottery.DIAMOND };
private static final int[] timesTpyes = new int[] { 1, 10 }; // 只能抽1次或10连抽
private static final LotteryService loggterService = new LotteryService();
static {
// 这个static代码块中的代码实际上正式开发应该读取策划的配置表。为了演示方便在这里通过程序生成数据
for (int lotteryType : lotteryTypes) {
for (int timesType : timesTpyes) {
ConfigLottery configLottery = new ConfigLottery();
configLottery.setLotteryType(lotteryType);
configLottery.setTimesType(timesType);
configLottery.setCost(10000 * timesType);
List<ConfigLotteryItem> items = new LinkedList<>();
items.add(new ConfigLotteryItem(1000 * lotteryType, 1, 10));
items.add(new ConfigLotteryItem(1001 * lotteryType, 2, 20));
items.add(new ConfigLotteryItem(1002 * lotteryType, 3, 30));
configLottery.setItems(items);
loggterService.addConfigLottery(configLottery);
}
}
}
public static void main(String[] args) {
Player player = null;
for (int lotteryType : lotteryTypes) {
for (int timesType : timesTpyes) {
System.out.println("Lottery#" + lotteryType + "#" + timesType);
for (int i = 0; i < 10; i++) {
player = newPlayer();
System.out.println("-- " + i);
loggterService.lottery(player, lotteryType, timesType);
}
}
}
}
private static Player newPlayer() {
Player player = new Player();
player.setSliver(100000);
player.setGold(100000);
player.setDiamond(100000);
return player;
}
}
以上,一个简单的测试代码就算完成了。
优化资源处理
终于做完需求了。但是,咱作为优秀的忍者,哦不,作为优秀的程序员。还记得之前提过的需要优化的地方吗。
回到最开始的Player代码。有木有发现,alterSliver、alterGold、alterDiamond这几个方法的实现和类似。虽然我们已经通过抽象逻辑,使用alter方法同时处理3种资源的修改逻辑。但是,还是不那么完美。比如,加入你游戏里面突然加入了一种新资源Power(体力)。你就得在Player中增加如下代码:
private static final int MAX_POWER = Integer.MAX_VALUE;
private int power;
public int getPower() {
return power;
}
public void setPower(int power) {
this.power = power;
}
public boolean alterPower(int alter) {
int old = power;
power = alter(power, alter, 0, MAX_POWER);
return old != power;
}
相信对代码充满的追求的同学已经开始思考怎样避免这种行为了。这里你可以先想一下,再继续看下面对Player的修改:
请先思考
请先思考
请先思考
下面是修改后的Player
package com.kakashi01.player.domain;
import java.util.HashMap;
import java.util.Map;
public class Player {
public static final int RESOURCE_SLIVER = 1;
public static final int RESOURCE_GOLD = 2;
public static final int RESOURCE_DIAMOND = 3;
public static final int RESOURCE_POWER = 4;
public static final int[] ALL_RESOURCES = {
RESOURCE_SLIVER,
RESOURCE_GOLD,
RESOURCE_DIAMOND,
RESOURCE_POWER };
private static final Map<Integer, Integer> RESOURCE_MAX = new HashMap<>();
static {
for (int resource : ALL_RESOURCES) {
RESOURCE_MAX.put(resource, Integer.MAX_VALUE);
}
}
private int id;
private Map<Integer, Integer> resources = new HashMap<>();
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getResource(int resource) {
Integer r = resources.get(resource);
if (r == null) {
return 0;
}
return r.intValue();
}
public void setResource(int resource, int value) {
resources.put(resource, value);
}
public boolean alterResource(int resource, int alter) {
int current = getResource(resource);
int old = current;
int min = 0;
int max = getMaxResource(resource);
if (alter > 0) {
current += alter;
if (current < min || current > max) {
current = max;
}
} else if (alter < 0) {
if (current >= -alter) {
current += alter;
}
if (current < min) {
current = min;
}
}
setResource(resource, current);
return old != current;
}
public static int getMaxResource(int resource) {
Integer r = RESOURCE_MAX.get(resource);
if (r == null) {
return 0;
}
return r.intValue();
}
}
可以看到修改后的Player代码对资源的处理更加统一,甚至说,忍者,哦不,开发者,可以对资源的类型不那么敏感,如果策划想增加另一种资源,其实程序只是定义多一个资源类型而已。不需要增加大段的代码。有的人说,快速完成功能才是王道,代码不需要好的设计。难道这个设计不是能让你更快速完成功能吗?因此,很多东西不是非此即彼。不是你代码写的差,你开发效率就高的。(偷笑,别砸我)
由于Player对资源处理进行了修改,那么相应的,修改LotteryService中的tryCostResource方法。修改后代码如下:
private boolean tryCostResource(Player player, ConfigLottery configLottery) {
return player.alterResource(configLottery.getLotteryType(), -configLottery.getCost());
}
看,又是一个好的设计减少了代码量的例子。LotteryDemo中的那几个setXXX方法,也要自己修改为setResource方法。
下班前的悬念
优化完了,也快到时间下班了。此时策划向你走了过来。拍了你的肩膀,向你投来崇拜的眼神,你已经完成了抽奖了呀。不错呀。不过。。。可能有几个需求还要改一下。策划向你留下了几个需求:
1. 每种宝箱有可能不是消耗对应的资源。比如,白银宝箱可能也可以消耗黄金来开启。
2. 10连抽现在只是简单的循环抽了10次,需要改成10连抽里面9次是正常抽的,而有1次会掉落更高级的东西。
此时如果是你,你会在上面的代码进行怎样的调整,使得适应新的需求。由于时间和篇幅的关系,下一篇文章会继续宝箱抽奖模块的制作。程序员和策划之间的斗智斗勇还在继续。
本文项目可以在https://github.com/johncha/CodeDesign-1找到。请先阅读git中的README.md查看项目的使用说明。
如果你对本文有什么建议或意见,可以发邮件到john.cha@qq.com或到blog.csdn.net/kakashi8841中留言。