Skip to content

ioGame 21.6,java 高性能游戏服务器框架;netty 高性能游戏服务器框架

Compare
Choose a tag to compare
@iohao iohao released this 23 Apr 07:41
· 176 commits to main since this release

本次更新

属性监听特性

#264 新增属性值变更监听特性

文档 : 属性监听 (yuque.com)

属性可添加监听器,当某些属性值的发生变化时,触发监听器。

使用场景举例

比如玩家的血量低于一定值时,需要触发无敌状态;此时,我们就可以监听玩家的血量,并在该属性上添加一个对应的监听器来观察血量的变化,当达到预期值时就触发对应的业务。

类似的使用场景还有很多,这里就不过多的举例了。属性监听的特点在于属性变化后会触发监听器。

属性监听特点

  • 可为属性添加监听器,用于观察属性值的变化。
  • 属性可以添加多个监听器。
  • 属性的监听器可以移除。

框架已经内置了几个属性实现类,分别是:

  • IntegerProperty
  • LongProperty
  • StringProperty
  • BooleanProperty
  • ObjectProperty

for example - 添加监听器

BooleanProperty

当 BooleanProperty 对象的值发生改变时,触发监听器。

var property = new BooleanProperty();
// 添加一个监听器。
property.addListener((observable, oldValue, newValue) -> {
   log.info("oldValue:{}, newValue:{}", oldValue, newValue);
});

property.get(); // value is false
property.set(true); // 值变更时,将会触发监听器
property.get(); // value is true

IntegerProperty

当 IntegerProperty 对象的值发生改变时,触发监听器。

var property = new IntegerProperty();
// add listener monitor property object
property.addListener((observable, oldValue, newValue) -> {
   log.info("oldValue:{}, newValue:{}", oldValue, newValue);
});

property.get(); // value is 0
property.set(22); // When the value changes,listeners are triggered
property.get(); // value is 22

property.increment(); // value is 23. will trigger listeners

for example - 移除监听器

下面这个示例,我们将 property 初始值设置为 10,随后添加了一个监听器;当监听器观察到新值为 9 时,就从 observable 中移除自己(这个自己指的是监听器本身),而 observable 则是 IntegerProperty。

@Test
public void remove1() {
    IntegerProperty property = new IntegerProperty(10);
    // 添加一个监听器
    property.addListener(new PropertyChangeListener<>() {
        @Override
        public void changed(PropertyValueObservable<? extends Number> observable, Number oldValue, Number newValue) {
            log.info("1 - newValue : {}", newValue);

            if (newValue.intValue() == 9) {
                // 移除当前监听器
                observable.removeListener(this);
            }
        }
    });

    property.decrement(); // value 是 9,并触发监听器
    property.decrement(); // value 是 8,由于监听器已经移除,所以不会触发任何事件。
}

下面的示例中,我们定义了一个监听器类 OnePropertyChangeListener 并实现了 PropertyChangeListener 监听器接口。示例中,我们通过 OnePropertyChangeListener 对象的引用来移除监听器。

@Test
public void remove2() {
    // 监听器
    OnePropertyChangeListener onePropertyChangeListener = new OnePropertyChangeListener();
    
    // 属性
    IntegerProperty property = new IntegerProperty();
    // 添加监听器
    property.addListener(onePropertyChangeListener);

    property.increment(); // value == 1,并触发监听器
    property.removeListener(onePropertyChangeListener); // 移除监听器
    property.increment(); // value == 2,由于监听器已经移除,所以不会触发任何事件。
}

// 自定义的监听器
class OnePropertyChangeListener implements PropertyChangeListener<Number> {
    @Override
    public void changed(PropertyValueObservable<? extends Number> observable, Number oldValue, Number newValue) {
        log.info("oldValue:{}, newValue:{}, observable:{}", oldValue, newValue, observable);
    }
}

属性监听 - 小结

属性监听在使用上是简单的,如果你的业务中有关于属性变化后需要触发某些事件的,可以考虑引用该特性。框架为 int、long、boolean、Object、String 等基础类型提供了对应的属性监听。

属性监听特性支持添加多个监听器,支持移除监听器。

模拟客户端相关

模拟客户端新增与服务器断开连接的方法。

模拟客户端新增是否活跃的状态属性。

ClientUser clientUser = ...;
// 是否活跃,true 表示玩家活跃
clientUser.isActive();
// 关闭模拟客户端连接
clientUser.getClientUserChannel().closeChannel();

获取游戏对外服的数据与扩展相关

文档 获取游戏对外服的数据与扩展 (yuque.com)

RequestCollectExternalMessage 增加 userId 字段。

#265 模拟玩家请求时 - 从游戏对外服中获取在线玩家相关数据

新增 UserHeadMetadataExternalBizRegion,从用户(玩家)所在游戏对外服中获取用户自身的数据,如用户所绑定的游戏逻辑服、元信息 ...等

使用参考

@Slf4j
@RestController
@RequestMapping("other")
public class OtherController {
    static final AtomicLong msgId = GameManagerController.msgId;
    /** 为了方便测试,这里指定一个 userId 来模拟玩家 */
    static final long userId = GameManagerController.userId;

    @GetMapping("/notice")
    public String notice() {
        log.info("other notice");
        // 使用协议碎片特性 https://www.yuque.com/iohao/game/ieimzn
        StringValue data = StringValue.of("other GM web msg " + msgId.incrementAndGet());
        // 模拟请求 : 路由 - 业务数据
        RequestMessage requestMessage = BarMessageKit.createRequestMessage(ExchangeCmd.of(ExchangeCmd.notice), data);

        // 设置需要模拟的玩家
        HeadMetadata headMetadata = requestMessage.getHeadMetadata();
        headMetadata.setUserId(userId);

        // 从游戏对外服中获取一些用户(玩家的)自身的数据,如元信息、所绑定的游戏逻辑服 ...等
        Optional<HeadMetadata> headMetadataOptional = ExternalCommunicationKit.employHeadMetadata(requestMessage);

        if (headMetadataOptional.isPresent()) {
            // 发起模拟请求
            extractedRequestLogic(requestMessage);

            // 打印从游戏对外服获取的元信息
            byte[] attachmentData = headMetadata.getAttachmentData();
            ExchangeAttachment attachment = DataCodecKit.decode(attachmentData, ExchangeAttachment.class);
            return "other notice 玩家的元信息: %s - %s".formatted(attachment, msgId.get());
        } else {
            return "other notice 玩家 %s 不在线,无法获取玩家的元信息 - %s".formatted(userId, msgId.get());
        }
    }

    private void extractedRequestLogic(RequestMessage requestMessage) {
        // 向逻辑服发送请求,该模拟请求具备了玩家的元信息。
        BrokerClient brokerClient = MyKit.brokerClient;
        InvokeModuleContext invokeModuleContext = brokerClient.getInvokeModuleContext();
        invokeModuleContext.invokeModuleVoidMessage(requestMessage);
    }
}

任务工具相关

TaskListener 接口增加异常回调方法 void onException(Throwable e),用于接收异常信息;当 triggerUpdate 或 onUpdate 方法抛出异常时,将会传递到该回调方法中。

@Test
public void testException() throws InterruptedException {
    AtomicBoolean hasEx = new AtomicBoolean(false);
    TaskKit.runOnce(new OnceTaskListener() {
        @Override
        public void onUpdate() {
            // 模拟一个业务异常
            throw new RuntimeException("hello exception");
        }

        @Override
        public void onException(Throwable e) {
            hasEx.set(true);
            // 触发异常后,将来到这里
            log.error(e.getMessage(), e);
        }
    }, 10, TimeUnit.MILLISECONDS);

    TimeUnit.MILLISECONDS.sleep(200);
    Assert.assertTrue(hasEx.get()); // true
}

业务框架相关 - [common-core]

#266 新增 RangeBroadcast 范围内的广播功能,这个范围指的是,可指定某些用户进行广播。

在执行广播前,开发者可以自定义业务逻辑,如

  • 添加一些需要广播的用户
  • 删除一些不需要接收广播的用户
  • 可通过重写 logic、trick 方法来做一些额外扩展

for example
在某些业务场景下,能让语义更清晰

// example - 1
new RangeBroadcast(flowContext)
        // 需要广播的数据
        .setResponseMessage(responseMessage)
        // 添加需要接收广播的用户
        .addUserId(1)
        .addUserId(2)
        .addUserId(List.of(3L, 4L, 5L))
        // 排除一些用户,被排除的用户将不会接收到广播
        .removeUserId(1)
        // 执行广播
        .execute();

// example - 2
new RangeBroadcast(flowContext)
        // 需要广播的数据
        .setResponseMessage(cmdInfo, playerReady)
        // 添加需要接收广播的用户
        .addUserId(1)
        // 执行广播
        .execute();

[light-game-room] 房间模块

移除 AbstractRoom broadcast 系列方法,开发者可使用 RoomBroadcastFlowContext 接口实现旧的兼容。

移除 AbstractRoom createSend 方法,开发者可使用 ofRangeBroadcast 系列来代替。AbstractRoom 新增 RoomBroadcastEnhance,实现房间内的广播增强,该系列在语义上更清晰。

这里使用一个准备就绪的示例来演示,当有玩家准备时,需要通知房间内的其他玩家

final RoomService roomService = ...;

@ActionMethod(RoomCmd.ready)
public void ready(boolean ready, FlowContext flowContext) {
    long userId = flowContext.getUserId();
    // 得到玩家所在的房间
    AbstractRoom room = this.roomService.getRoomByUserId(userId);
    
    // 准备
    PlayerReady playerReady = new PlayerReady();
    playerReady.userId = userId;
    playerReady.ready = ready;
  
    // 通知房间内的所有玩家,有玩家准备或取消准备了
    room.ofRangeBroadcast(flowContext)
            // 响应数据(路由、业务数据)
            .setResponseMessage(flowContext.getCmdInfo(), playerReady)
            .execute();
}

// 准备或取消准备
@ProtobufClass
@FieldDefaults(level = AccessLevel.PUBLIC)
public class PlayerReady {
    /** 当前操作的玩家 userId */
    long userId;
    /** true 表示准备 */
    boolean ready;
}

AbstractRoom 增加 ifPlayerExist、ifPlayerNotExist 方法。

ifPlayerExist 方法

如果玩家在房间内,就执行给定的操作,否则不执行任何操作。

这里演示玩家加入房间的业务

RoomService roomService = ...;
AbstractRoom room = ...;
// 如果玩家不在房间内,就创建一个玩家,并让玩家加入房间
room.ifPlayerNotExist(userId, () -> {
    // 玩家加入房间
    FightPlayerEntity newPlayer = new FightPlayerEntity();
    newPlayer.setId(userId);
    
    this.roomService.addPlayer(room, newPlayer);
});

ifPlayerNotExist 方法

如果玩家不在房间内,就执行给定的操作,否则不执行任何操作。

这里结合 RangeBroadcast,演示当有新玩家加入房间时,通知房间内的其他玩家。

AbstractRoom room = ...;
// 有新玩家加入房间,通知其他玩家
room.ifPlayerExist(userId, (FightPlayerEntity playerEntity) -> {
    FightPlayer fightPlayer = FightMapstruct.ME.convert(playerEntity);
    room.ofRangeBroadcast(flowContext)
            .setResponseMessage(RoomCmd.of(RoomCmd.playerEnterRoomBroadcast), fightPlayer)
            // 排除不需要通知的玩家(当前 userId 是自己)
            .removeUserId(userId)
            .execute();
});