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

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

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

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

// 从地图上查找附近的游戏对象
final List<WorldObject> 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<Npc, Long> PENDING_RESPAWNS 集合。这个集合的Key就是死亡npc,而Long值就是再次复活的时间。该线程会不停的从PENDING_RESPAWNS 集合获取死亡的npc,然后根据时间判断是否需要复活。

// 当前时间
final long time = System.currentTimeMillis();

// 循环死亡的npc
for (Entry<Npc, Long> 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(裙文件里面也可以下载所有内容)。

猜你喜欢

转载自blog.csdn.net/konkon2012/article/details/134000049