从零开始手写mmo游戏从框架到爆炸(二十二)— 战斗系统三

本文详细介绍了如何从零开始构建一个MMO游戏,包括地图设定、战斗引擎的设计以及服务端和客户端的战斗相关处理。作者通过实例展示了如何创建地图、搜索野怪和组织战斗过程。
摘要由CSDN通过智能技术生成

 导航:从零开始手写mmo游戏从框架到爆炸(零)—— 导航-CSDN博客

       

目录

地图设定

战斗引擎 

服务端的BattleHandler

客户端的相关handler

战斗场景展示

执行效果


        文接上一章。我们把战斗系统demo应用到实际的项目中来。在第十九章(从零开始手写mmo游戏从框架到爆炸(十九)— 用户数据封装与存储-CSDN博客)中,客户端已经可以创建英雄并且进入游戏主界面了,下一步,我们需要选择地图,并且和地图中的野怪来一张遭遇战。

      首先我们把上一章中的test路径下的demo移动到正式路径下并进行修改。新增的几个类如下:

  

        其中大部分上一章已经基本讲过了,大家对着gitee上的分支源码来看即可,我们重点关注地图已经搜寻野怪的代码。

地图设定

地图:TownMap

@Data
public class TownMap implements Serializable {

    private String id;

    private FindMonstersEngine findMonsterEngine;

    // 级别 1 就是 0-9级  9 就是 90-99级
    private int level;

    private int floorId;

    private String name;

    private String desc;

    private String teamSize;

    // 可能出现的野怪
    private List<MonsterInMap> monsters;

    public TownMap(MapTemplate mapTemplate) {
        this.findMonsterEngine = new DefaultFindMonsters();
        this.name = mapTemplate.getName();
        this.level = mapTemplate.getLevel();
        this.monsters = MapFactory.createMonsters(mapTemplate);
        this.desc = mapTemplate.getDesc();
        this.teamSize = mapTemplate.getTeamSize();
        this.id = mapTemplate.getId();
    }
}

        地图中存储了野怪的等级已经出现的概率。同时增加了一个接口-FindMonstersEngine。

我们来看下这个接口:

public interface FindMonstersEngine {

    List<Monster> findMonsters(HeroWrapper hero, TownMap map) throws IOException, ClassNotFoundException, Exception;
}

这个接口就一个方法,寻找野怪。我们写一个默认的实现类:

public class DefaultFindMonsters extends AbstractFindMonsters{

    @Override
    public List<Monster> findMonsters(HeroWrapper heroWrapper, TownMap map) throws Exception {
        List<Monster> result = new ArrayList<>();
        // 根据
        List<MonsterInMap> monsters = map.getMonsters();

        String[] teamSize = map.getTeamSize().split("-");
        int min = Integer.parseInt(teamSize[0]);
        int max = Integer.parseInt(teamSize[1]);

        int num;
        if(min == max){
            num = max;
        }else {
            num = min + new Random().nextInt(max - min);
        }
        for (int i = 0; i < num; i++) {
            Monster monster = RandomBalance.getMonsters(monsters);
            result.add(monster);
        }
        return BeanCopyUtils.deepCopy(result);
    }
}

战斗引擎 

        我们再看下战斗引擎 BattleEngine

public class BattleEngine {

    // 队列不变
    private final LinkedList<Attack> actions = new LinkedList<>();

    private int addAction(Attack action){
        actions.offer(action);
        return actions.size();
    }

    public BattleFightResult fight(List<? extends Character> listA, List<? extends Character> listB) throws InterruptedException {
        BattleFightResult result = new BattleFightResult();
        List<String> fightLogs = result.getFightLogs();
        // 先初始化
        listA.sort(Comparator.comparing(Character::getSpeed).reversed());
        for (int i = 0; i < listA.size(); i++) {
            addAction(new GroupAttack(listA.get(i),listB));
        }

        // 再放入listB
        listB.sort(Comparator.comparing(Character::getSpeed).reversed());
        for (int i = 0; i < listB.size(); i++) {
            GroupAttack attack = new GroupAttack(listB.get(i), listA);
            insertAction(attack);
        }

        // 如果A集合和B集合的生命值都还大于0

        while(listA.stream().anyMatch(e -> e.getCurrentHp() > 0) && listB.stream().anyMatch(e -> e.getCurrentHp() > 0)) {
            Attack pop = actions.pop();
            AttackResult run = pop.run();
            if(run != null && run.getSuccess()) {
                System.out.println(run.getContent());
                fightLogs.addAll(run.getContent());
                // 再放进去
                if (pop.checkContinue()) {
                    // 要重新计算interval的时间
                    pop.setIntervalTime(pop.getIntervalTime() + pop.computeInterval(pop.speed()));
                    insertAction(pop);
                }
            }

        }

        if(listA.stream().anyMatch(e -> e.getCurrentHp() > 0)) {
            result.setWin(true);
        }else{
            result.setWin(false);
        }
        return result;
    }

    private void insertAction(Attack attack) {
        int intervalTime = attack.getIntervalTime();

        // 如果第一个就大于attack的interval
        if(actions.get(0).getIntervalTime() > attack.intervalTime()){
            // 在头插入一个
            actions.push(attack);
        }
        else {
            ListIterator<Attack> iterator = actions.listIterator();
            while (iterator.hasNext()) {
                Attack next = iterator.next();

                if (next.getIntervalTime() > intervalTime) {
                    break;
                }
            }
            // 在指定位置插入新元素
            iterator.add(attack);
        }
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        Hero heroA = JobFactory.createHero(JobFactory.getTemplates().get(0),"haha");
        Hero heroB = JobFactory.createHero(JobFactory.getTemplates().get(0),"erer");

        BattleEngine engine = new BattleEngine();
        BattleFightResult fight = engine.fight(Arrays.asList(heroA), Arrays.asList(heroB));

    }

}

服务端的BattleHandler

@Component
@TopicListener(topic = ServerTopic.TOPIC_BATTLE)
public class BattleHandler extends BaseHandler {

    public static final Logger log = LoggerFactory.getLogger(BattleHandler.class);

    @TagListener(tag = ServerBattleTag.TAG_BATTLE_MAPS,messageClass = StringRequest.class)
    public StringMessage mapList(ChannelHandlerContext ctx, StringRequest data) throws Exception {
        List<MapTemplate> mapTemplates = MapFactory.mapTemplates();
        // 获取地图列表
        StringMessage message = StringMessage.create(ClientTopic.TOPIC_MENU, ClientMenuTag.TAG_MENU_MAP_CHOOSE);
        message.setStatusCode(CommonValue.MSG_STATUS_CODE_SUCCESS);

        MapChoose mapChoose = new MapChoose(message);
        mapChoose.setMapList(MapTemplateConvert.INSTANCE.toMapItems(mapTemplates));

        message.setBody(JSON.toJSONString(mapChoose));
        return message;
    }

    @TagListener(tag = ServerBattleTag.TAG_BATTLE_COMMON_FIGHT,messageClass = FightChoose.class)
    public StringMessage commonFight(ChannelHandlerContext ctx, FightChoose data) throws Exception {

        TownMap townMap = MapFactory.createMap(MapFactory.getMapById(data.getMapId()));

        UserGameWrapper gameWrapper = ctx.channel().attr(SessionAttributeKey.GAME).get();
        HeroWrapper currentHero = gameWrapper.getCurrentHero();
        Hero hero = currentHero.getHero();

        // 计算野怪的类型 数目 等级 品质
        List<Monster> monsters = townMap.getFindMonsterEngine().findMonsters(currentHero, townMap);
        BattleEngine engine = new BattleEngine();
        BattleFightResult fight = engine.fight(Arrays.asList(hero), monsters);

        // 获取地图列表
        StringMessage message = StringMessage.create(data.getSuccessCallbackTopic(), data.getSuccessCallbackTag());
        message.setStatusCode(CommonValue.MSG_STATUS_CODE_SUCCESS);
        message.setBody(JSON.toJSONString(fight));
        return message;
    }
}

客户端的相关handler

选择地图:MenuHandler

    /***
     * 选择地图
     * @param ctx
     * @param data
     */
    @TagListener(tag = ClientMenuTag.TAG_MENU_MAP_CHOOSE, messageClass = MapChoose.class)
    public void chooseMaps(ChannelHandlerContext ctx, MapChoose data) {
        //
        ConsolePrint.publishMessage("请选择要进入的地图");
        List<MapChoose.MapItem> mapList = data.getMapList();
        for (int i = 0; i < mapList.size(); i++) {
            ConsolePrint.publishMessage((i+1) + ":" + mapList.get(i).toString(), 1);
        }
        Scanner input = new Scanner(System.in);
        int choose = input.nextInt();
        while(choose < 0 || choose > mapList.size()){
            ConsolePrint.publishMessage("请重新选择", 1);
            choose = input.nextInt();
        }
        MapChoose.MapItem item = mapList.get(choose - 1);
        ConsolePrint.publishMessage("选择的地图为" + item.getName());

        // 继续选择是打怪还是返回
        ConsolePrint.publishMessage("请选择您要进行的操作");
        ConsolePrint.publishMessage("【1.打怪寻宝】  【2.挑战BOSS】  【3.返回】");
        choose = ScannerInput.inputInt(1, 3, 3);
        if(choose != 3) {
            switch (choose) {
                case 1 :
                    // 战斗
                    StringMessage message = StringMessage.create(ServerTopic.TOPIC_BATTLE, ServerBattleTag.TAG_BATTLE_COMMON_FIGHT);
                    message.setStatusCode(CommonValue.MSG_STATUS_CODE_SUCCESS);
                    FightChoose request = new FightChoose(ClientTopic.TOPIC_SCENE, ClientSceneTag.TAG_SCENE_BATTLE, ClientTopic.TOPIC_MENU,ClientMenuTag.TAG_MENU_MAP_CHOOSE);
                    request.setMapId(item.getId());
                    request.setType(1);
                    message.setBody(JSON.toJSONString(request));
                    ChannelUtils.pushToServer(ctx.channel(), message);
                    break;
                case 2:
                    // 战斗
                    ConsolePrint.publishMessage("暂未开放,敬请期待", 1);
                    // break;
                default:
                    // 返回上一页
                    NetworkClientListener.getInstance().forward(ctx,ClientTopic.TOPIC_MENU, ClientMenuTag.TAG_MENU_MAP_CHOOSE, JSON.toJSONString(data));
            }
        }
        else {
            // 返回上一页
            NetworkClientListener.getInstance().forward(ctx,ClientTopic.TOPIC_MENU, ClientMenuTag.TAG_MENU_MAP_CHOOSE, JSON.toJSONString(data));
        }
    }

战斗场景展示

        SceneHandler

@TopicListener(topic = ClientTopic.TOPIC_SCENE)
public class SceneHandler extends BaseHandler {

    public static final Logger log = LoggerFactory.getLogger(SceneHandler.class);

    @TagListener(tag = ClientSceneTag.TAG_SCENE_BATTLE,messageClass = BattleFightResult.class)
    public void portalMenu(ChannelHandlerContext ctx, BattleFightResult result){
        ConsolePrint.publishMessage("战斗开始");

        List<String> fightLogs = result.getFightLogs();
        for (String fightLog : fightLogs) {
            ConsolePrint.publishMessage(fightLog);
        }

        if(result.isWin()){
            ConsolePrint.publishMessage("您胜利了");
        }else {
            ConsolePrint.publishMessage("您失败了");
        }
    }

    /***
     * 选择地图
     * @param ctx
     * @param data
     */
    @TagListener(tag = ClientMenuTag.TAG_MENU_MAP_CHOOSE, messageClass = MapChoose.class)
    public void chooseMaps(ChannelHandlerContext ctx, MapChoose data) {
        //
        ConsolePrint.publishMessage("请选择要进入的地图");
        List<MapChoose.MapItem> mapList = data.getMapList();
        for (int i = 0; i < mapList.size(); i++) {
            ConsolePrint.publishMessage((i+1) + ":" + mapList.get(i).toString(), 1);
        }
        Scanner input = new Scanner(System.in);
        int choose = input.nextInt();
        while(choose < 0 || choose > mapList.size()){
            ConsolePrint.publishMessage("请重新选择", 1);
            choose = input.nextInt();
        }
        MapChoose.MapItem item = mapList.get(choose - 1);
        ConsolePrint.publishMessage("选择的地图为" + item.getName());

        // 继续选择是打怪还是返回
        ConsolePrint.publishMessage("请选择您要进行的操作");
        ConsolePrint.publishMessage("【1.打怪寻宝】  【2.挑战BOSS】  【3.返回】");
        choose = ScannerInput.inputInt(1, 3, 3);
        if(choose != 3) {
            switch (choose) {
                case 1 :
                    // 战斗
                    StringMessage message = StringMessage.create(ServerTopic.TOPIC_BATTLE, ServerBattleTag.TAG_BATTLE_COMMON_FIGHT);
                    message.setStatusCode(CommonValue.MSG_STATUS_CODE_SUCCESS);
                    FightChoose request = new FightChoose(ClientTopic.TOPIC_SCENE, ClientSceneTag.TAG_SCENE_BATTLE, ClientTopic.TOPIC_MENU,ClientMenuTag.TAG_MENU_MAP_CHOOSE);
                    request.setMapId(item.getId());
                    request.setType(1);
                    message.setBody(JSON.toJSONString(request));
                    ChannelUtils.pushToServer(ctx.channel(), message);
                    break;
                case 2:
                    // 战斗
                    ConsolePrint.publishMessage("暂未开放,敬请期待", 1);
                    // break;
                default:
                    // 返回上一页
                    NetworkClientListener.getInstance().forward(ctx,ClientTopic.TOPIC_MENU, ClientMenuTag.TAG_MENU_MAP_CHOOSE, JSON.toJSONString(data));
            }
        }
        else {
            // 返回上一页
            NetworkClientListener.getInstance().forward(ctx,ClientTopic.TOPIC_MENU, ClientMenuTag.TAG_MENU_MAP_CHOOSE, JSON.toJSONString(data));
        }
    }
}

执行效果

【1.登录已有账户】  【2.注册新账户】  【3.退出】
请选择:
1
请输入账户名:
eric
请输入密码:
111111
234:234
请选择要创建的职业
1:JobChoose.JobItem(id=1, name=战士, desc=剑类武器, strength=60, armature=5, constitution=80, magic=0, technique=15, speed=130)
1
选择的职业为战士
请输入角色的名称:
haer
124:124
英雄创建成功
请选择您要进行的操作
【1.打怪】  【2.装备】  【3.战兽】
【4.冒险者工会】   【5.副本】  【6.工会】 
【8.配置】  【9.退出】
请选择:
1
196:196
请选择要进入的地图
1:地图 { 名称 ='新手村', 级别 =1, 说明 ='新手练级的地方,适合等级1-10级'}
1
选择的地图为新手村
请选择您要进行的操作
【1.打怪寻宝】  【2.挑战BOSS】  【3.返回】
1
2831:2831
战斗开始
haer[800/800] 攻击了地精战士,造成伤害60点
地精头目[730/730] 攻击了haer,造成伤害65点
地精战士[160/220] 攻击了haer,造成伤害22点
haer[713/800] 攻击了地精战士,造成伤害60点
地精头目[730/730] 攻击了haer,造成伤害65点
地精战士[100/220] 攻击了haer,造成伤害22点
haer[626/800] 攻击了地精战士,造成伤害60点
地精头目[730/730] 攻击了haer,造成伤害65点
地精战士[40/220] 攻击了haer,造成伤害22点
haer[539/800] 攻击了地精战士,造成伤害60点
地精头目[730/730] 攻击了haer,造成伤害65点
haer[474/800] 攻击了地精头目,造成伤害60点
地精头目[670/730] 攻击了haer,造成伤害65点
haer[409/800] 攻击了地精头目,造成伤害60点
地精头目[610/730] 攻击了haer,造成伤害65点
haer[344/800] 攻击了地精头目,造成伤害60点
地精头目[550/730] 攻击了haer,造成伤害65点
haer[279/800] 攻击了地精头目,造成伤害60点
地精头目[490/730] 攻击了haer,造成伤害65点
haer[214/800] 攻击了地精头目,造成伤害60点
地精头目[430/730] 攻击了haer,造成伤害65点
haer[149/800] 攻击了地精头目,造成伤害60点
地精头目[370/730] 攻击了haer,造成伤害65点
haer[84/800] 攻击了地精头目,造成伤害60点
地精头目[310/730] 攻击了haer,造成伤害65点
haer[19/800] 攻击了地精头目,造成伤害60点
地精头目[250/730] 攻击了haer,造成伤害65点
您失败了

全部源码详见:

gitee : eternity-online: 多人在线mmo游戏 - Gitee.com

分支:step-12

请各位帅哥靓女帮忙去gitee上点个星星,谢谢!

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值