Java游戏项目编码注意事项

本文探讨了Java游戏服务器开发中的关键注意事项,包括减少运算、优化配置处理、避免不必要的数据库操作、确保并发安全以及提高代码可读性。强调了配置不应直接存储在业务类中,提倡使用单例获取配置信息,同时指出在处理并发场景时要注意线程安全问题,并提倡先扣减后增益的逻辑,以简化异常处理。此外,还提到了日志记录的规范和代码结构的整洁性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 编程总纲

    1. 服务器负责主导整个系统的流程,必须对整个系统有清晰的认知,而不是策划主导.(包括表的设计),一切设计应以减少整体的工作量为目标,如果相同的工作量做取舍,优先级应是服务器>客户端>策划.

    2. 能提前运算的尽量提前运算,能缓存的尽量缓存.而不是在实际的业务去做复杂的运算(比如配置中复杂格式解析,应该在起服之后就进行,而不是在业务使用的时候进行)

    3. 尽量减少不必要的运算, 例如:发送协议给多人的时候, 就应该是Message Build一次,都发这一份build出来的数据(以现有代码举例, 那么gate和其它进程之间应该存在一个广播协议, 包头带上玩家id, 包体是给每个玩家爱的消息)

    4. 设计大型系统的时候想办法进行计算错峰,应尽量避免毛刺现象,比如同时进入副本,结算。此类特殊状态节点应予以评估。

  2. 配置相关

    1. 自定义类中不允许以任何方式缓存配置文件, 即AbstractConfigData继承类不可以以成员变量的形式存在于业务的类中.(受框架管理的IConfigCache继承不在此列) 。缓存会导致reload过程中无法更新业务自己缓存的配置.

      /**
       * new关键字用于创建一个新的对象实例,而getInstance()方法通常用于获取一个单例对象(Singleton)的实例
       * 用于获取怪物配置信息
       * @return
       */
      public MonsterConfig getMonsterConfig() {
          // ConfigDataManager.getInstance(): 调用ConfigDataManager类的静态方法getInstance(),以获取该类的单例实例。得到这个实例后再调用这个实例中的getById()方法,传入两个参数:MonsterConfig.class(怪物配置类用于封装)和this.getConfigId()(具体哪一个怪物) 不同id不同怪物
          return ConfigDataManager.getInstance().getById(MonsterConfig.class, this.getConfigId());
      }
      public class BloodyCastleDupMap extends DevilSquareDupMap {
      ​
          /**配置文件不可以直接定义在自定义类中获取需要通过反射获取通过对应的id获取对应的怪兽类型
           * 血色城堡配置信息
           */
          private BloodyCastleConditionConfig bloodyConfig;
      1. 正确例子 ,只存储id,实际需要配置的时候从DataManager中取.

      2. 错误例子,config文件被定义在自定义类里面.

    2. 不在进程运行的过程中进行一些累计运算, 如增加的属性,消耗。即如果有业务系统是从1-10级。那你取消耗的时候应该10级就有对应的消耗数据,而不是 1+2+3.。+10运算得出一个10级的消耗。 提前计算好

  3. 数据库设计相关

    1. 不可为了方便计算,直接配置对应的相关属性直接存储在db之中.这样会导致配置修改之后属性无法同步修改,需要编写代码去修复

      1. 正确示例

        //这段代码是一个名为RoleMiniGame的Java类,其中定义了一个私有整数变量miniGameId。这个变量被标记为@Tag(1),可能是用于标识或分类的标签。
        //在类的使用部分,首先通过调用ConfigDataManager.getInstance()获取一个单例对象,然后调用该对象的getById()方法来根据MiniGameConfig类
        //和role.getMiniGame().getMiniGameId()作为参数获取相应的配置信息。将获取到的配置信息存储在miniGameConfig变量中。
        //定义的地方
        public class RoleMiniGame {
            /**
             * {@link com.sh.game.common.config.model.minigame.MiniGameConfig}
             */
            @Tag(1)
            private int miniGameId;
         }
         MiniGameConfig miniGameConfig = ConfigDataManager.getInstance().getById(MiniGameConfig.class,role.getMiniGame().getMiniGameId());
        ​
         //接下来,判断miniGameConfig是否为非空。如果非空,则遍历miniGameConfig.getAttribute()返回的映射(Map)中的键值对,并使用attributeTuple.merge()方法将这些键值对合并到holder对象中。
         //使用的地方
        if (miniGameConfig != null) {
            for (Map.Entry<Integer, Integer> entry : miniGameConfig.getAttribute().entrySet()) {
                attributeTuple.merge(holder, entry.getKey(), entry.getValue());
            }
        //总结,这段代码的作用是根据角色的miniGameId获取相应的游戏配置信息,并将配置属性合并到holder对象中。
        }
      2. 错误示例.

        //此为RoleBag 的背包最大数, 
        @Tag(3)
        private int maxCount ; 
    2. 查询数据库的函数如果可能被线程访问,应考虑多线程并发问题

      1. 错误示例

        //此类写法当多线程同时查询,会导致每个线程调用这个函数返回的值可能不一致.
        //用于获取角色摊位(RoleStall)的信息
        public RoleStall getRoleStall(long rid) {
            //这一行从缓存中获取与给定角色ID对应的角色摊位信息。如果缓存中存在该信息,则将其赋值给roleStall变量。
            RoleStall roleStall = cache.get(rid, DataType.ROLE_STALL);
            //这是一个条件判断语句,检查roleStall是否为null。如果为null,则执行以下代码块
            if (roleStall == null) {
                //调用hasRole方法来检查是否存在具有给定角色ID的角色
                if (hasRole(rid)) {
                    //这一行使用模板查询方法(template.query)从持久化工厂(RoleStallPersistFactory)中查询角色摊位信息,并将结果赋值给roleStall变量
                    roleStall = this.template.query(RoleStallPersistFactory.SELECT, RoleStallPersistFactory.MAPPER, rid);
                }
                if (roleStall != null) {
                    //这个put 应该采用类似ConcurrentHashMap之类的putIfAbsent并且接收返回值。
                    cache.put(roleStall);//这里注意并发问题 多线程
                }
            }
            if (roleStall != null) {
                //则调用roleStall对象的touch方法更新最后访问时间。
                roleStall.touch(TimeUtil.getNowOfMills());
            }
            return roleStall;
        }
    3. 我们的数据的entity是通过protostuff序列化二进制存储到db中, 所以一个entity不允许被多个对象缓存.比如,我有一个AEntity, 被BEntity以成员变量形式 同时又被CEntity以成员变量形式持有. 这种就会反序列化之后 B和C 都有一份Entity 并不是同一份, 这种情况应该是通过id 或者类似标识的方式间接持有

  4. 客户端协议输入相关

    1. 参数需要注意校验 比如负数,或者极大值

    2. 发list列表要做好去重校验

    3. 客户端任何能输入的只要有收益的地方都需要做好逻辑校验.

  5. 小的细节

    1. 日志的打印应尽量做到 三要素 什么人(玩家或者actor) 在什么地方(发生的上下文) 干了什么事(行为)

      1. 错误的示例

        if (pointList.isEmpty()){
            log.error("pointList为空");
            return;
        }
      2. 正确的示例

        //config.getPreposeUnlock()的值不等于0,并且config.getPreposeUnlock()的值不等于roleMiniGame.getMiniGameId()的值
        if (config.getPreposeUnlock() != 0 && config.getPreposeUnlock() != roleMiniGame.getMiniGameId()) {
            TipUtil.error(rid, "请按顺序升级");
            //向用户显示一条错误消息,提示他们需要按照特定的顺序进行升级。rid是用户的唯一标识符,用于识别特定的用户
            log.error("rid:{} cur miniGameId:{}, cfgId:{}", rid, roleMiniGame.getMiniGameId(), cfgId);
            //rid、roleMiniGame.getMiniGameId()和cfgId分别表示用户的唯一标识符、角色的最小游戏ID和配置ID。这些信息可以帮助开发人员诊断问题并追踪错误发生的位置。
            return;
        }
    2. 枚举不要使用频繁使用values函数, 枚举的values每次都拷贝和enum等长的object的数组,应该自己用map去维护。

    3. 不要加太多无用的判断, 所有的入参加一堆null的判断, 任何代码都加上保护说明对自己代码要运行的场景没有清晰的认知.

    4. 在玩家和系统进行交换的时候(比如购买,回收等), 应先扣再给,这样哪怕流程出问题了。后续给玩家补发即可。如果先给玩家东西再扣想要再追就很麻烦了。

  6. 代码可读性相关

    1. 在编码时 ,尽量把不满足执行逻辑的条件提前返回或退出 , 否则代码中可能会出现过多括号嵌套增加他人阅读代码的难度 , 这样也会增加代码重构的难度

    2. 因为你写的代码不仅仅是自己看 ,别人也需要看

      1. 反面教材

        public void Test(){
            boolean condition = .......
            if (condition) {
                if (param.length > 2) {
                    if (arg != null && arg.length > 0) {
                        Buffer buffer = (Buffer) arg[0];
                        if (buffer instanceof BufferHasAttribute) {
                            if (s != null) {
                                // 向Buffer的属性映射中添加指定的属性值
                                ((BufferHasAttribute) buffer).getAttribute().put(s, value);
                            }
                        }
                    } 
                }
            }
        }
      2. 推荐写法

        public void Test() {
            boolean condition = .......
            if (!condition) {
                return;
            }
            if (!param.length > 2) {
                return;
            }
            if (!(arg != null && arg.length > 0)) {
                return;
            }
            Buffer buffer = (Buffer) arg[0];
            if (!buffer instanceof BufferHasAttribute) {
                if (s != null) {
                    // 向Buffer的属性映射中添加指定的属性值
                    ((BufferHasAttribute) buffer).getAttribute().put(s, value);
                }
            }
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值