方便适用的计数器Counter

为什么要使用计数器?

在游戏程序的开发中,我们会遇到很多跟计数相关的需求,比如玩家领取了多少次奖励、成就的任务进度、一场比赛中的得分等等。然而在很多的API里,很少提供我们不用关心边界值或中间操作的计数器,特别是对于服务器来讲,基本会使用有键值对的计数器,因为我们管理的是一群玩家,并不是一个,当然可能还会有更多的层级,比如一群玩家的任务进度计数,就是一个3维数组。要实现这种看似简单的功能,我们就会想到Java里Map这个东西,但是很遗憾的是,Map的处理太过多余了,他并不是为计数而生,我们需要改造一下,这里先从简单计数器说起。

AtomicInteger

这个AtomicInteger好啊,又是线程安全的,又有方便的计数API,我们就从他出发吧。

计数器的API总共就几个,获取当前值(get),直接设置当前值(set),增加多少值(getAndAdd和addAndGet),其实就相当于操作符++,一个是a++,一个是++a。在增加值的基础上再提供++1、- -1、1- -、1++这种操作,基本上就够用了,对于AtomicInteger的原理这里就不赘述。

IntCounter

既然有线程安全的了,为啥要有一个非安全的,嘛。。毕竟线程安全的有那么点点以牺牲性能为代价。这里提一下游戏服务器里的线程模型,假设是一个以地图为基础的RPG游戏,通常来讲,我们会以地图来分线程,保证同一个地图的玩家是在同一个线程上的(这就是为什么有些游戏交易必须同地图,甚至更老的游戏,功能都绑定在了NPC身上,题外话不多说)。如果该功能对于玩家是单机性质(自己的操作不影响他人)的或者说即使交互(与其他玩家发生行为)也是同地图玩家的交互,计数操作是没必要线程安全的。RPG游戏是高反馈低延时的游戏,所以扣点性能也没什么大惊小怪的(虽然现在硬件已经很变态了)。

IntCounter的实现其实非常简单,就是对int的再次封装(LongCounter同理),想必我这里不说,大家都明白怎么写这个代码了。举个栗子,代码没什么好讲的,大家可以实现更多方便的API,虽然这些API总共就没几行,但是积少成多,对于项目的开大有着很大的帮助,不要小看他了。

    private int count;
    /**
     * 归零
     */
    public void zero() {
        setCount(0);
    }

    /**
     * 设置为最大值
     */
    public void setHigh() {
        setCount(high());
    }

    /**
     * 设置为最小值
     */
    public void setLow() {
        setCount(low());
    }

    /**
     * 最大限制
     *
     * @return
     */
    public int high() {
        return Integer.MAX_VALUE;
    }

    /**
     * 最小限制
     *
     * @return
     */
    public int low() {
        return 0;
    }

    /**
     * 获取当前数值
     *
     * @return
     */
    public int getCount() {
        return this.count;
    }

    /**
     * 直接设置当前值
     *
     * @param value
     * @return
     */
    protected int setCount(int value) {
        return this.count = GameMathUtil.fixedBetween(value, low(), high());
    }

    /**
     * +1并获取
     *
     * @return
     */
    public int incrementAndGet() {
        return addAndGet(1);
    }

    /**
     * -1并获取
     *
     * @return
     */
    public int decrementAndGet() {
        return addAndGet(-1);
    }

    /**
     * 增加并获取
     *
     * @param delta
     * @return
     */
    public int addAndGet(int delta) {
        return setCount(getCount() + delta);
    }

    /**
     * 获取并+1
     *
     * @return
     */
    public int getAndIncrement() {
        return getAndAdd(1);
    }

    /**
     * 获取并-1
     *
     * @return
     */
    public int getAndDecrement() {
        return getAndAdd(-1);
    }

    /**
     * 获取并增加
     *
     * @param delta
     * @return
     */
    public int getAndAdd(int delta) {
        int old = getCount();
        setCount(GameMathUtil.safeAdd(old, delta));
        return old;
    }

键值对Map类型计数器的包装

刚才说了,不管是对于服务器本身的性质也好还是对于需求本身也好,都会存在同类型复数个计数器,我们就自然想到了Map(2维映射)类型甚至Table(3维映射)来处理这个事情。由于原本自带的Map并不是专门做这种事情的,所以我们针对计数器的需求特别优化一下API的友好度。

如果我们直接使用Map的话,我们必须要处理

1.不管在放入计数或者是获取计数的时候是否存在一个键值对,如果不存在我们会初始化他

2.如果大部分的计数在常规状态下都为初始值(这里假设为0),那么我们会初始化一堆没有用的数据

3.每次计数改变的操作,都会先取出数据(取出的时候还要做第1步的检查),然后更改数值再放回,这些代码重复太多会让写代码的人不能直接关注需求本身而产生混乱从而导致很多BUG。

下面的代码简单的演示下上面的痛处

//声明一个Map
Map<String,Integer> tasks = new HashMap<>();
//现在获取任务"kill monster"的进度
String taskName = "kill monster";
Integer taskProccess = tasks.get(taskName);
//如果任务进度为空则初始化任务进度为0
if(taskProccess == null){
    taskProccess = 0;
    tasks.put(taskName,taskProccess);
}
//任务进度+1
taskProccess+=1;
//这里由于惯性思维,在写很多复杂逻辑的时候很有可能会不做put操作而导致bug
tasks.put(taskName,taskProccess);

上面的操作简直让人蛋疼无比!那么我们先看看经过优化过的IntMap是怎么写代码的吧!

IntMap<String> tasksNew = IntMap.empty();
tasksNew.incrementAndGet(taskName);

两行代码解决,是不是轻松多了,不算上声明,1行代码解决,本来计数这种简单操作就应该是一行代码操作的事情,对吧。

针对上面的伤痛,我们看看是怎么实现一个自己的IntMap。计数器的API都是大同小异的

/**
 * 获取计数
 *
 * @param key
 * @return
 */
int getCount(K key);

/**
 * 设置计数
 *
 * @param key
 * @param newValue
 * @return
 */
int putCount(K key, int newValue);

/**
 * 总数
 *
 * @return
 */
int sum();

/**
 * 加1并获取
 *
 * @param key
 * @return
 */
int incrementAndGet(K key);

/**
 * 减1并获取
 *
 * @param key
 * @return
 */
int decrementAndGet(K key);

/**
 * 增加并获取
 *
 * @param key
 * @param delta
 * @return
 */
int addAndGet(K key, int delta);

/**
 * 获取并加1
 *
 * @param key
 * @return
 */
int getAndIncrement(K key);

/**
 * 获取并减1
 *
 * @param key
 * @return
 */
int getAndDecrement(K key);

/**
 * 获取并增加
 *
 * @param key
 * @param delta
 * @return
 */
int getAndAdd(K key, int delta);

我们使用Java8中Map的新API(Map.compute)可以非常方便的实现刚才Map中冗余的操作

/**
 * 获取并更新
 *
 * @param key
 * @param updaterFunction
 * @return
 */
private int getAndUpdate(K key, IntUnaryOperator updaterFunction) {
    AtomicInteger holder = new AtomicInteger();
    map.compute(key, (k, value) -> {
        // 如果获取key的value为空,则直接返回0
        int oldValue = (value == null) ? 0 : value;
        holder.set(oldValue);
        return updaterFunction.applyAsInt(oldValue);
    });
    return holder.get();
}

获取并更新实现了,更新并获取就大同小异了,其余的API也只是对这个基础方法进行包装而已

键值对更特殊的优化-枚举计数器EnumIntCounter

枚举是个非常好的东西,让代码看起来非常简洁,不混乱,有明确定义,主要是有一种限定作用,避免产生参数值的错误。我们来想象一个需求,统计星期1-7当中,哪个星期玩家杀怪的数量最多,这里我们就可以把星期1-7做成一个枚举

 

public enum WEEK{
    W_1,
    W_2,
    W_3,
    W_4,
    W_5,
    W_6,
    W_7,
}

这里不用星期的英文是因为我懒得去查了(属于说得出来拼不出来,看见又认识的那种,哈哈,野生英语水平)。既然枚举叫枚举,那在代码运行期间,他的数量肯定是一定的,所以我们在表示这种结构的时候不会像Map那样复杂,单纯的用一个int数组(int[])就行了,至于大小,刚才不是说了吗,枚举是固定的,所以我们就这样声明

private int[] counts;

public static <E extends Enum<E>> EnumIntCounter<E> create(Class<E> enumClass) {
    return new EnumIntCounter<>(enumClass);
}

public EnumIntCounter(Class<E> enumClass) {
    counts = new int[EnumUtil.length(enumClass)];
}

这里获取枚举长度用的EnumUtil.length其实就是c.getEnumConstants().length,c是枚举Class。

实现get和put对于数组来说就非常简单了,只需要提供数据的index去做更改就行了。至于默认值为0,数据本身new出来就全部默认是0了。有时候还是需要通过枚举的编号去获取计数的,所以我们还得为get和put分别提供一个传int编号过来查找计数的重载方法

 

/**
    * 获取计数
    *
    * @param e
    * @return
    */
   public int getCount(E e) {
       return getCount(e.ordinal());
   }

   private int getCount(int ordinal) {
       if (ordinal > counts.length - 1 || ordinal < 0) {
           return 0;
       }
       return counts[ordinal];
   }

   /**
    * 放置计数
    *
    * @param e
    * @param value
    * @return
    */
   public int putCount(E e, int value) {
       return putCount(e.ordinal(), value);
   }

   private int putCount(int ordinal, int value) {
       // 容错
       if (ordinal > counts.length - 1) {
           int[] temp = new int[ordinal + 1];
           System.arraycopy(counts, 0, temp, 0, counts.length);
           counts = temp;
       }
       int old = counts[ordinal];
       counts[ordinal] = value;
       return old;
   }

同样的,实现了get和put,什么增加、减少、加一、减一我相信你自己就能搞定,就不赘述了。总之,这些东西虽然看起来非常简单,感觉人人都能想到,但是真正跑过去优化的人不多,自己总是抱怨写起烦躁,但是就是不动手搞一搞。我在项目上用到的这3种计数器让我写复杂逻辑的时候不再关心如何计数,身心健康,心旷神怡,再也不再想锤策划人员一顿了~好处就是这么多。

如果你也实现完了,我们来看看怎么用吧,巨简单!

EnumIntCounter<WEEK> tasksNewAgain = new EnumIntCounter<>(WEEK.class);
tasksNewAgain.incrementAndGet(WEEK.W_1);

哎呀!我去,舒服!

告辞!

  1. HankXV说道:

    :razz: 本尊来试试回复功能是不是扯淡的~

    1. HankXV说道:

      回复的回复是不是扯淡的 :confused:

发表评论

电子邮件地址不会被公开。 必填项已用*标注