本章节,我们学习一下玩家周边怪物的刷新。在上一章节中,我们提过这个事情。当玩家移动完毕之后,会显示周围的游戏对象,其中就包括NPC怪物。当然,玩家“孵化”自己(调用spawnMe方法)的时候,也会显示周围的游戏对象。我们首先看一下玩家“孵化”自己的时候,调用的是WorldObject 的spawnMe 方法,在这个方法中重要的一句代码:
World.getInstance().addVisibleObject(this, getWorldRegion(), null);
我们继续到World 类中查看addVisibleObject 方法,如下所示
- // 从地图上查找附近的游戏对象
- final List
visibleObjects = getVisibleObjects(object, 2000); - for (int i = 0; i < visibleObjects.size(); i++)
- {
- // 周围的对象把 当前角色"我" 加入到 _knownObjects 列表中
- wo.getKnownList().addKnownObject(object, dropper);
-
- // 当前角色"我" 把 周围对象加入到 _knownObjects 列表中
- object.getKnownList().addKnownObject(wo, dropper);
- }
我们重点查看最后一句代码:object.getKnownList().addKnownObject(wo, dropper); 也就是,当前玩家把周围的游戏对象(NPC怪物)添加到自己的_knownObjects 列表中。这里需要注意的是,游戏玩家的getKnownList() 方法返回的是PlayerKnownList 类,它的addKnownObject方法如下:
- else if (object.isNpc())
- {
- activeChar.sendPacket(new NpcInfo((Npc) object, activeChar));
- }
该代码会根据游戏对象的类型,向玩家客户端发送不同数据,这里的NpcInfo就是(NPC怪物)对应的数据包信息。接下来,我们再来看游戏角色移动完毕之后的操作,也就是游戏角色Creature类中的updatePosition方法最后的代码部分
- // 到达目标点之后,更新周围游戏对象
- if (distFraction > 1)
- {
- getKnownList().updateKnownObjects();
- ThreadPool.execute(() -> getAI().notifyEvent(CtrlEvent.EVT_ARRIVED));
- return true;
- }
我们继续查看PlayerKnownList 类的updateKnownObjects的方法,其实这个方法位于父类WorldObjectKnownList中,代码如下
- if (_activeObject instanceof Creature)
- {
- findCloseObjects();
- forgetObjects();
- }
我们继续查看findCloseObjects 方法,代码如下
- if (_activeObject.isPlayable()){
- for (WorldObject object : World.getInstance().getVisibleObjects(_activeObject))
- {
- addKnownObject(object);
- }}
这里大家一定不要忘记Java的多态,我们实例化的是子类PlayerKnownList,即使我们调用了WorldObjectKnownList里面的addKnownObject方法,它还是会调用PlayerKnownList里面的重写的addKnownObject方法的。上面我们已经介绍过这个方法了,它就是向玩家客户端发送NpcInfo数据包。
既然我们玩家身边已经出现了NPC怪物,那么我们就可以对其进行攻击了。首先,我们应该点击选择我们要攻击的对象(NPC怪物)。此时,会向服务器端发送Action数据包。这个Action数据包的应用比较广泛,我们后期还会遇到它。我们查看这个Action数据包。
- private int _objectId; // 鼠标点击选中的游戏对象ID
- private int _originX; // 玩家当前位置
- private int _originY; // 玩家当前位置
- private int _originZ; // 玩家当前位置
接下来,我们继续查看run方法
- // 鼠标点击选中的游戏对象(根据ID查询)
- final WorldObject obj = World.getInstance().findObject(_objectId);
- obj.onAction(player);
我们先根据游戏对象ID来找到这个游戏对象实例,紧接着就会调用游戏对象的onAction方法。这里要注意的是,调用的是NPC怪物的onAction方法,不是玩家Player的onAction方法。接下来,我们就去怪物类Monster的onAction方法。实际上,这个方法是在它的父类Npc中,我们去父类Npc中查看,这个onAction方法的参数是当前玩家哦。在这个方法中,分为两种情况。一种是Npc怪物不是当前玩家Player的目标对象_target,另一种就是Npc怪物是当前玩家Player的目标对象。当我们第一次选中Npc怪物的时候,它当然不是当前玩家的目标对象,因此执行第一种情况的代码。
- if (this != player.getTarget())
- {
- // 设置当前玩家的选择目标
- player.setTarget(this);
- // 发送 MyTargetSelected 数据包
- player.sendPacket(new MyTargetSelected(getObjectId(), 0));
- // 设置开始攻击时间
- player.setTimerToAttack(System.currentTimeMillis());
- // 校验玩家当前位置
- player.sendPacket(new ValidateLocation(this));
- }
以上代码就是设置当前玩家已经选中的鼠标点击的游戏对象(Npc怪物),然后向客户端发送MyTargetSelected数据包,其实就是告诉客户端,服务器端已经选中了,可以进行下一步操作了。接下来,我们就可以继续单击我们鼠标选中的游戏对象(Npc怪物)。那么,客户端依然向服务器端发送Action数据包,依然会调用怪物类Monster的onAction方法。当时,由于我们前面的操作中已经设置了玩家的目标对象,因此这里该执行第二种情况。
- // 校验玩家当前位置
- player.sendPacket(new ValidateLocation(this));
- // 设置玩家为攻击状态
- player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this);
这里会调用玩家的PlayerAI类让其进入到AI_INTENTION_ATTACK 攻击状态。这个setIntention方法实际位于父类AbstractAI中,
- case AI_INTENTION_ATTACK:
- {
- onIntentionAttack((Creature) arg0);
- break;
- }
上面的onIntentionAttack方法实际位于CreatureAI类,参数就是攻击对象。这里由分为两种情况,一种是当今玩家已经处于攻击状态(防止用户多次点击攻击相同目标),另一种就是当前玩家不是攻击状态。显然,我们属于后者,我们查看对应的代码
- // 改变玩家的状态
- changeIntention(AI_INTENTION_ATTACK, target, null);
- // 设置攻击目标
- setAttackTarget(target);
- // 停止移动
- stopFollow();
- // 执行 EVT_THINK
- notifyEvent(CtrlEvent.EVT_THINK, null);
这里,我们重点查看最后一句代码:notifyEvent(CtrlEvent.EVT_THINK, null); 这个notifyEvent方法位于父类AbstractAI中,代码如下
- case EVT_THINK:
- {
- onEvtThink();
- break;
- }
上面的onEvtThink是在PlayerAI类中,它会根据不同状态执行不同行为,这个onEvtThink方法实际上循环执行的。因为玩家的自动攻击就是有AI进行循环执行。那么循环的开始位置就是这里的onEvtThink方法。那么循环的代码在哪里呢?我们往后看就明白了。
- if (getIntention() == AI_INTENTION_ATTACK)
- {
- // 自动攻击
- thinkAttack();
- }
这里不用说,一定是要执行thinkAttack方法的,而这个方法最终会调用Player的doAttack方法,这个方法的代码逻辑并不多,主要在它的父类Creature中的doAttack方法,它的参数就是被攻击的对象,我们大致介绍一下这个方法。
- // 获取手持武器
- final Weapon weaponItem = getActiveWeaponItem();
- final Item weaponInst = getActiveWeaponInstance();
-
- // 检查灵魂蛋使用
- boolean wasSSCharged;
-
- // 根据武器计算攻击时间
- final int timeAtk = calculateTimeBetweenAttacks(target, weaponItem);
-
- // 攻击到一半的时候,给与目标伤害
- final int timeToHit = timeAtk / 2;
-
- // 本次攻击结束时间
- _attackEndTime = GameTimeTaskManager.getInstance().getGameTicks();
- _attackEndTime += (timeAtk / GameTimeTaskManager.MILLIS_IN_TICK);
- _attackEndTime -= 1;
-
- // 武器的等级
- int ssGrade = 0;
-
- // 发送给客户端的攻击数据包
- final Attack attack = new Attack(this, wasSSCharged, ssGrade);
-
- // 计算下次攻击时间
- final int reuse = calculateReuseTime(target, weaponItem);
-
- // 是否产生伤害(可能miss哦)
- hitted = doAttackHitSimple(attack, target, timeToHit);
-
- // 更新玩家PVP状态
- player.updatePvPStatus(target);
-
- // miss效果
- if (!hitted){
- sendPacket(new SystemMessage(SystemMessageId.YOU_HAVE_MISSED));
- abortAttack();
- }
-
- // 如果命中造成伤害就广播Attack数据包
- if (attack.hasHits())
- {
- broadcastPacket(attack);
- }
-
- // 定时任务执行 NotifyAITask 任务(就是执行L2PlayerAI 中的 onEvtThink 方法)
- ThreadPool.schedule(new NotifyAITask(CtrlEvent.EVT_READY_TO_ACT), timeAtk + reuse);
请注意,上面的NotifyAITask任务会执行L2PlayerAI 中的 onEvtThink 方法。在上面的说明中,我们已经说了,这个onEvtThink方法实际上循环执行的。什么时候结束呢?要么玩家取消攻击,要么怪物死亡等等情况发送。其实就是取消玩家的AI_INTENTION_ATTACK状态即可。接下来,我们在简单说一下上面的doAttackHitSimple方法。
- // 攻击是否miss
- final boolean miss1 = Formulas.calcHitMiss(this, target);
-
- // 计算伤害值
- damage1 = (int) Formulas.calcPhysDam(this, target, null, shld1, crit1, false, attack.soulshot);
-
- // timeToHit 时间后执行 HitTask 伤害任务。攻击动作到一半的时候造成伤害。
- ThreadPool.schedule(new HitTask(target, damage1, crit1, miss1, attack.soulshot, shld1), sAtk);
-
- // 攻击数据包中添加伤害值
- attack.addHit(target, damage1, miss1, crit1, shld1);
上面的HitTask伤害任务就是执行:
onHitTimer(_hitTarget, _damage, _crit, _miss, _soulshot, _shld);
我们直接介绍onHitTimer 方法即可。
- // 发送伤害信息,就是SystemMessage 数据包。
- sendDamageMessage(target, damage, false, crit, miss);
-
- // 计算吸血(增加玩家HP)
- final double absorbPercent = getStat().calcStat(Stat.ABSORB_DAMAGE_PERCENT, 0, null, null);
- setCurrentHp(getStatus().getCurrentHp() + absorbDamage);
-
- // 计算反射伤害(减少玩家HP)
- final double reflectPercent = target.getStat().calcStat(Stat.REFLECT_DAMAGE_PERCENT, 0, null, null);
- getStatus().reduceHp(reflectedDamage, target, true);
-
- // 减少怪物目标HP(怪物死亡后掉落物品最为奖励)
- target.reduceCurrentHp(damage, this);
-
- // 设置怪物开始反击玩家
- target.getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, this);
-
- // 发送开始自动攻击数据包,就是AutoAttackStart 数据包
- getAI().clientStartAutoAttack();
-
这需要大家注意的是,上面的主要攻击代码都是集中在Creature类。这个类,我们之前讲解过,它是玩家Player和怪物Monster的父类,里面的移动代码是共享的。当然,对于攻击也是如此,也是共享于玩家和怪物的。也就是说,上面的怪物开始反击玩家的代码也在Creature类中。两者不同的地方在于AI类是不一样的。但是,AI类最终还是调用的Creature类doAttack方法。在这个doAttack方法中,会根据当前的角色实例(Player或Monster)来进行不同的代码逻辑判断。这里就不再详细介绍了。
玩家和怪物结束战斗的情况,第一就是两者距离问题,第二就是一方死亡。第一个距离问题涉及到两者相互追逐的情况。如果是玩家逃跑的话,玩家就自动放弃主动攻击的状态,而转入移动的状态;怪物可能会追击(仍然是战斗状态)。如果能追击上,就发起攻击,不能追击上,就转入正常的状态(返回出生点进入巡逻状态)。第二个就是一方死亡,双方都会停止自动攻击。如果怪物死亡,就会掉落物品。如果是玩家死亡,就会弹框给与提示(原地复活还是回到附近村庄)。如果一方死亡的话,另一方都会改变状态。例如,玩家会停止自动攻击的状态;怪物也会停止自动攻击进入正常状态。战斗双方在“自动战斗”过程中都是使用定时器完成的。结束战斗的话,就需要取消定时器。
怪物死亡后重新复活是在RespawnTaskManager类中管理的,他是一个单例类,同时也是一个线程。在这个线程类中,有一个Map
- // 当前时间
- final long time = System.currentTimeMillis();
-
- // 循环死亡的npc
- for (Entry
entry : PENDING_RESPAWNS.entrySet()) - {
- // 如果到了复活的时间就复活
- if (time > entry.getValue().longValue())
- {
- // 复活npc
- spawn.respawnNpc(npc);
- }
- }
复活代码就是调用 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(裙文件里面也可以下载所有内容)。