第一章-宝箱抽奖模块与代码设计(一)

第一章-宝箱抽奖模块与代码设计(一)

简要信息
作者卡卡
博客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中留言。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值