• 第十三章:L2JMobius学习 – 玩家攻击怪物


    本章节,我们学习一下玩家周边怪物的刷新。在上一章节中,我们提过这个事情。当玩家移动完毕之后,会显示周围的游戏对象,其中就包括NPC怪物。当然,玩家“孵化”自己(调用spawnMe方法)的时候,也会显示周围的游戏对象。我们首先看一下玩家“孵化”自己的时候,调用的是WorldObject 的spawnMe 方法,在这个方法中重要的一句代码:

    World.getInstance().addVisibleObject(this, getWorldRegion(), null);

    我们继续到World 类中查看addVisibleObject 方法,如下所示

    1. // 从地图上查找附近的游戏对象
    2. final List visibleObjects = getVisibleObjects(object, 2000);
    3. for (int i = 0; i < visibleObjects.size(); i++)
    4. {
    5. // 周围的对象把 当前角色"我" 加入到 _knownObjects 列表中
    6. wo.getKnownList().addKnownObject(object, dropper);
    7. // 当前角色"我" 把 周围对象加入到 _knownObjects 列表中
    8. object.getKnownList().addKnownObject(wo, dropper);
    9. }

    我们重点查看最后一句代码:object.getKnownList().addKnownObject(wo, dropper); 也就是,当前玩家把周围的游戏对象(NPC怪物)添加到自己的_knownObjects 列表中。这里需要注意的是,游戏玩家的getKnownList() 方法返回的是PlayerKnownList 类,它的addKnownObject方法如下:

    1. else if (object.isNpc())
    2. {
    3. activeChar.sendPacket(new NpcInfo((Npc) object, activeChar));
    4. }

    该代码会根据游戏对象的类型,向玩家客户端发送不同数据,这里的NpcInfo就是(NPC怪物)对应的数据包信息。接下来,我们再来看游戏角色移动完毕之后的操作,也就是游戏角色Creature类中的updatePosition方法最后的代码部分

    1. // 到达目标点之后,更新周围游戏对象
    2. if (distFraction > 1)
    3. {
    4. getKnownList().updateKnownObjects();
    5. ThreadPool.execute(() -> getAI().notifyEvent(CtrlEvent.EVT_ARRIVED));
    6. return true;
    7. }

    我们继续查看PlayerKnownList 类的updateKnownObjects的方法,其实这个方法位于父类WorldObjectKnownList中,代码如下

    1. if (_activeObject instanceof Creature)
    2. {
    3. findCloseObjects();
    4. forgetObjects();
    5. }

    我们继续查看findCloseObjects 方法,代码如下

    1. if (_activeObject.isPlayable()){
    2. for (WorldObject object : World.getInstance().getVisibleObjects(_activeObject))
    3. {
    4. addKnownObject(object);
    5. }}

    这里大家一定不要忘记Java的多态,我们实例化的是子类PlayerKnownList,即使我们调用了WorldObjectKnownList里面的addKnownObject方法,它还是会调用PlayerKnownList里面的重写的addKnownObject方法的。上面我们已经介绍过这个方法了,它就是向玩家客户端发送NpcInfo数据包

    既然我们玩家身边已经出现了NPC怪物,那么我们就可以对其进行攻击了。首先,我们应该点击选择我们要攻击的对象(NPC怪物)。此时,会向服务器端发送Action数据包。这个Action数据包的应用比较广泛,我们后期还会遇到它。我们查看这个Action数据包。

    1. private int _objectId; // 鼠标点击选中的游戏对象ID
    2. private int _originX; // 玩家当前位置
    3. private int _originY; // 玩家当前位置
    4. private int _originZ; // 玩家当前位置

    接下来,我们继续查看run方法

    1. // 鼠标点击选中的游戏对象(根据ID查询)
    2. final WorldObject obj = World.getInstance().findObject(_objectId);
    3. obj.onAction(player);

    我们先根据游戏对象ID来找到这个游戏对象实例,紧接着就会调用游戏对象的onAction方法。这里要注意的是,调用的是NPC怪物的onAction方法,不是玩家Player的onAction方法。接下来,我们就去怪物类Monster的onAction方法。实际上,这个方法是在它的父类Npc中,我们去父类Npc中查看,这个onAction方法的参数是当前玩家哦。在这个方法中,分为两种情况。一种是Npc怪物不是当前玩家Player的目标对象_target,另一种就是Npc怪物是当前玩家Player的目标对象。当我们第一次选中Npc怪物的时候,它当然不是当前玩家的目标对象,因此执行第一种情况的代码。

    1. if (this != player.getTarget())
    2. {
    3. // 设置当前玩家的选择目标
    4. player.setTarget(this);
    5. // 发送 MyTargetSelected 数据包
    6. player.sendPacket(new MyTargetSelected(getObjectId(), 0));
    7. // 设置开始攻击时间
    8. player.setTimerToAttack(System.currentTimeMillis());
    9. // 校验玩家当前位置
    10. player.sendPacket(new ValidateLocation(this));
    11. }

    以上代码就是设置当前玩家已经选中的鼠标点击的游戏对象(Npc怪物),然后向客户端发送MyTargetSelected数据包,其实就是告诉客户端,服务器端已经选中了,可以进行下一步操作了。接下来,我们就可以继续单击我们鼠标选中的游戏对象(Npc怪物)。那么,客户端依然向服务器端发送Action数据包,依然会调用怪物类Monster的onAction方法。当时,由于我们前面的操作中已经设置了玩家的目标对象,因此这里该执行第二种情况。

    1. // 校验玩家当前位置
    2. player.sendPacket(new ValidateLocation(this));
    3. // 设置玩家为攻击状态
    4. player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this);

    这里会调用玩家的PlayerAI类让其进入到AI_INTENTION_ATTACK 攻击状态。这个setIntention方法实际位于父类AbstractAI中,

    1. case AI_INTENTION_ATTACK:
    2. {
    3. onIntentionAttack((Creature) arg0);
    4. break;
    5. }

    上面的onIntentionAttack方法实际位于CreatureAI类,参数就是攻击对象。这里由分为两种情况,一种是当今玩家已经处于攻击状态(防止用户多次点击攻击相同目标),另一种就是当前玩家不是攻击状态。显然,我们属于后者,我们查看对应的代码

    1. // 改变玩家的状态
    2. changeIntention(AI_INTENTION_ATTACK, target, null);
    3. // 设置攻击目标
    4. setAttackTarget(target);
    5. // 停止移动
    6. stopFollow();
    7. // 执行 EVT_THINK
    8. notifyEvent(CtrlEvent.EVT_THINK, null);

    这里,我们重点查看最后一句代码:notifyEvent(CtrlEvent.EVT_THINK, null); 这个notifyEvent方法位于父类AbstractAI中,代码如下

    1. case EVT_THINK:
    2. {
    3. onEvtThink();
    4. break;
    5. }

    上面的onEvtThink是在PlayerAI类中,它会根据不同状态执行不同行为,这个onEvtThink方法实际上循环执行的。因为玩家的自动攻击就是有AI进行循环执行。那么循环的开始位置就是这里的onEvtThink方法。那么循环的代码在哪里呢?我们往后看就明白了。

    1. if (getIntention() == AI_INTENTION_ATTACK)
    2. {
    3. // 自动攻击
    4. thinkAttack();
    5. }

    这里不用说,一定是要执行thinkAttack方法的,而这个方法最终会调用Player的doAttack方法,这个方法的代码逻辑并不多,主要在它的父类Creature中的doAttack方法,它的参数就是被攻击的对象,我们大致介绍一下这个方法。

    1. // 获取手持武器
    2. final Weapon weaponItem = getActiveWeaponItem();
    3. final Item weaponInst = getActiveWeaponInstance();
    4. // 检查灵魂蛋使用
    5. boolean wasSSCharged;
    6. // 根据武器计算攻击时间
    7. final int timeAtk = calculateTimeBetweenAttacks(target, weaponItem);
    8. // 攻击到一半的时候,给与目标伤害
    9. final int timeToHit = timeAtk / 2;
    10. // 本次攻击结束时间
    11. _attackEndTime = GameTimeTaskManager.getInstance().getGameTicks();
    12. _attackEndTime += (timeAtk / GameTimeTaskManager.MILLIS_IN_TICK);
    13. _attackEndTime -= 1;
    14. // 武器的等级
    15. int ssGrade = 0;
    16. // 发送给客户端的攻击数据包
    17. final Attack attack = new Attack(this, wasSSCharged, ssGrade);
    18. // 计算下次攻击时间
    19. final int reuse = calculateReuseTime(target, weaponItem);
    20. // 是否产生伤害(可能miss哦)
    21. hitted = doAttackHitSimple(attack, target, timeToHit);
    22. // 更新玩家PVP状态
    23. player.updatePvPStatus(target);
    24. // miss效果
    25. if (!hitted){
    26. sendPacket(new SystemMessage(SystemMessageId.YOU_HAVE_MISSED));
    27. abortAttack();
    28. }
    29. // 如果命中造成伤害就广播Attack数据包
    30. if (attack.hasHits())
    31. {
    32. broadcastPacket(attack);
    33. }
    34. // 定时任务执行 NotifyAITask 任务(就是执行L2PlayerAI 中的 onEvtThink 方法)
    35. ThreadPool.schedule(new NotifyAITask(CtrlEvent.EVT_READY_TO_ACT), timeAtk + reuse);

    请注意,上面的NotifyAITask任务会执行L2PlayerAI 中的 onEvtThink 方法。在上面的说明中,我们已经说了,这个onEvtThink方法实际上循环执行的。什么时候结束呢?要么玩家取消攻击,要么怪物死亡等等情况发送。其实就是取消玩家的AI_INTENTION_ATTACK状态即可。接下来,我们在简单说一下上面的doAttackHitSimple方法。

    1. // 攻击是否miss
    2. final boolean miss1 = Formulas.calcHitMiss(this, target);
    3. // 计算伤害值
    4. damage1 = (int) Formulas.calcPhysDam(this, target, null, shld1, crit1, false, attack.soulshot);
    5. // timeToHit 时间后执行 HitTask 伤害任务。攻击动作到一半的时候造成伤害。
    6. ThreadPool.schedule(new HitTask(target, damage1, crit1, miss1, attack.soulshot, shld1), sAtk);
    7. // 攻击数据包中添加伤害值
    8. attack.addHit(target, damage1, miss1, crit1, shld1);

    上面的HitTask伤害任务就是执行:

    onHitTimer(_hitTarget, _damage, _crit, _miss, _soulshot, _shld);

    我们直接介绍onHitTimer 方法即可。

    1. // 发送伤害信息,就是SystemMessage 数据包。
    2. sendDamageMessage(target, damage, false, crit, miss);
    3. // 计算吸血(增加玩家HP)
    4. final double absorbPercent = getStat().calcStat(Stat.ABSORB_DAMAGE_PERCENT, 0, null, null);
    5. setCurrentHp(getStatus().getCurrentHp() + absorbDamage);
    6. // 计算反射伤害(减少玩家HP)
    7. final double reflectPercent = target.getStat().calcStat(Stat.REFLECT_DAMAGE_PERCENT, 0, null, null);
    8. getStatus().reduceHp(reflectedDamage, target, true);
    9. // 减少怪物目标HP(怪物死亡后掉落物品最为奖励)
    10. target.reduceCurrentHp(damage, this);
    11. // 设置怪物开始反击玩家
    12. target.getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, this);
    13. // 发送开始自动攻击数据包,就是AutoAttackStart 数据包
    14. getAI().clientStartAutoAttack();

    这需要大家注意的是,上面的主要攻击代码都是集中在Creature类。这个类,我们之前讲解过,它是玩家Player和怪物Monster的父类,里面的移动代码是共享的。当然,对于攻击也是如此,也是共享于玩家和怪物的。也就是说,上面的怪物开始反击玩家的代码也在Creature类中。两者不同的地方在于AI类是不一样的。但是,AI类最终还是调用的Creature类doAttack方法。在这个doAttack方法中,会根据当前的角色实例(Player或Monster)来进行不同的代码逻辑判断。这里就不再详细介绍了。

    玩家和怪物结束战斗的情况,第一就是两者距离问题,第二就是一方死亡。第一个距离问题涉及到两者相互追逐的情况。如果是玩家逃跑的话,玩家就自动放弃主动攻击的状态,而转入移动的状态;怪物可能会追击(仍然是战斗状态)。如果能追击上,就发起攻击,不能追击上,就转入正常的状态(返回出生点进入巡逻状态)。第二个就是一方死亡,双方都会停止自动攻击。如果怪物死亡,就会掉落物品。如果是玩家死亡,就会弹框给与提示(原地复活还是回到附近村庄)。如果一方死亡的话,另一方都会改变状态。例如,玩家会停止自动攻击的状态;怪物也会停止自动攻击进入正常状态。战斗双方在“自动战斗”过程中都是使用定时器完成的。结束战斗的话,就需要取消定时器。

    怪物死亡后重新复活是在RespawnTaskManager类中管理的,他是一个单例类,同时也是一个线程。在这个线程类中,有一个Map PENDING_RESPAWNS 集合。这个集合的Key就是死亡npc,而Long值就是再次复活的时间。该线程会不停的从PENDING_RESPAWNS 集合获取死亡的npc,然后根据时间判断是否需要复活。

    1. // 当前时间
    2. final long time = System.currentTimeMillis();
    3. // 循环死亡的npc
    4. for (Entry entry : PENDING_RESPAWNS.entrySet())
    5. {
    6. // 如果到了复活的时间就复活
    7. if (time > entry.getValue().longValue())
    8. {
    9. // 复活npc
    10. spawn.respawnNpc(npc);
    11. }
    12. }

    复活代码就是调用 Spawn类的respawnNpc方法,在这个方法里面就直接调用initializeNpc(oldNpc) 方法,重新初始化当前的npc对象。initializeNpc方法我们之前已经讲解过了,这里不再叙述了。那么这个RespawnTaskManager 类哪里调用呢?就是在这个Spawn类中的decreaseCount方法中,

    RespawnTaskManager.getInstance().add(oldNpc, System.currentTimeMillis() + _respawnDelay);

    这个respawnDelay时间就是来自于孵化数据表spawnlist中的respawn_delay字段值。只不过在Spawn类中要做一个小设置:_respawnDelay = value < 10 ? 10000 : value * 1000;

    也就是说,如果这个respawn_delay字段值小于10的话,就修改为10000(10秒),否者就乘以1000(换算成毫秒级单位)。那么,这个Spawn类中的decreaseCount方法谁来调用?就是在npc类的onDecay方法中。该方法是在DecayTaskManager中调用的,这也是一个单例线程类。而DecayTaskManager的调用是在npc类的doDie方法中调用。看到doDie方法,大家应该就非常清除了,就是怪物死亡时候调用的方法。

    本章节涉及的内容均已上传百度网盘:

    https://pan.baidu.com/s/1XdlcCFPvXnzfwFoVK7Sn7Q?pwd=avd4

    欢迎加企鹅交流裙:874700842(裙文件里面也可以下载所有内容)。

  • 相关阅读:
    c语言入门--数组
    掌握Python爬虫实现网站关键词扩展提升曝光率
    typescript对类型的管理和查找规则
    六边形架构浅析
    阿里云/腾讯云国际站代理:国际腾讯云的优势
    自动出价下机制设计系列 (二) : 面向私有约束的激励兼容机制设计
    封装公共组件中在main.js中通过插件统一注册
    OAuth 2.1 框架
    学习JAVA第五课:常用API
    《style scope》 作用域保护如何修改(组件库)子组件的样式
  • 原文地址:https://blog.csdn.net/konkon2012/article/details/134000049