第十三章: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;	// 鼠标点击选中的游戏对象IDprivate 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()){// 复活npcspawn.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(裙文件里面也可以下载所有内容)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/119378.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hadoop分布式安装

首先准备好三台服务器或者虚拟机&#xff0c;我本机安装了三个虚拟机&#xff0c;安装虚拟机的步骤参考我之前的一篇 virtualBox虚拟机安装多个主机访问虚拟机虚拟机访问外网配置-CSDN博客 jdk安装 参考文档&#xff1a;Linux 环境下安装JDK1.8并配置环境变量_linux安装jdk1.8并…

Zoho Mail荣登福布斯2023年企业邮箱榜单,引领行业新方向!

几十年来&#xff0c;电子邮件一直是电子通信的重要形式&#xff0c;并且在未来的许多年里&#xff0c;它可能会无处不在。尽管有大量免费电子邮件服务可供用户和企业使用&#xff0c;但其中许多服务缺乏专门的功能&#xff0c;例如适合办公室使用的集中管理。 福布斯小型企业顾…

1024特别剪辑: 使用Python Turtle 库绘制一棵随机生成的树

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;强烈推荐优质专栏: &#x1f354;&#x1f35f;&#x1f32f;C的世界(持续更新中) &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;…

MYSQL(事务+锁+MVCC+SQL执行流程)理解(2)

一)MYSQL中的锁(知识补充) 可以通过In_use字段来进行判断是否针对于表进行加了锁 1)对于undo log日志来说:新增类型的&#xff0c;在事务提交之后就可以清除掉了&#xff0c;修改类型的&#xff0c;事务提交之后不能立即清除掉这些日志会用于mvcc只有当没有事务用到该版本信息时…

【原创】解决Kotlin无法使用@Slf4j注解的问题

前言 主要还是辟谣之前的网上的用法&#xff0c;当然也会给出最终的使用方法。这可是Kotlin&#xff0c;关Slf4j何事&#xff01;&#xff1f; 辟谣内容&#xff1a;创建注解来解决这个问题 例如&#xff1a; Target(AnnotationTarget.CLASS) Retention(AnnotationRetentio…

等离子体共振和ENZ模式的场增强效应提高ITO对THz产生的非线性响应

利用等离子体共振和ENZ模式的场增强效应提高ITO对THz产生的非线性响应,SRR超表面的共振被设计成与λENZ紧密对齐。然而&#xff0c;ITO薄膜的加入&#xff0c;强烈地改变了透射光谱。 在x偏振照明下观察到共振分裂,在λENZ周围出现了一个明显的透明窗口,类似于等离子体诱导的透…

Spark内核调度

目录 一、DAG &#xff08;1&#xff09;概念 &#xff08;2&#xff09;Job和Action关系 &#xff08;3&#xff09;DAG的宽窄依赖关系和阶段划分 二、Spark内存迭代计算 三、spark的并行度 &#xff08;1&#xff09;并行度设置 &#xff08;2&#xff09;集群中如何规划并…

javascript IP地址正则表达式

/^(1[0-9]{2}|2[0-4][0-9]|25[0-5]|(\d){1,2})\.(1[0-9]{2}|2[0-4][0-9]|25[0-5]|(\d){1,2}|0)\.(1[0-9]{2}|2[0-4][0-9]|25[0-5]|(\d){1,2}|0)\.(1[0-9]{2}|2[0-4][0-9]|25[0-5]|(\d){1,2}|0)$/g.test(10.2.35.8) 注&#xff1a; 一定不要把表达式赋值给变量&#xff0c;直接…

软考-网络安全审计技术原理与应用

本文为作者学习文章&#xff0c;按作者习惯写成&#xff0c;如有错误或需要追加内容请留言&#xff08;不喜勿喷&#xff09; 本文为追加文章&#xff0c;后期慢慢追加 by 2023年10月 网络安全审计概念 等级保护网络安全审计是指对涉及国家安全、重要利益或关键信息基础设施…

FPGA/SoC控制机械臂

FPGA/SoC控制机械臂 机器人技术处于工业 4.0、人工智能和边缘革命的前沿。让我们看看如何创建 FPGA 控制的机器人手臂。 介绍 机器人技术与人工智能和机器学习一起处于工业 4.0 和边缘革命的最前沿。 因此&#xff0c;我认为创建一个基础机器人手臂项目会很有趣&#xff0c;我们…

听GPT 讲Rust源代码--library/std(3)

rust标准库std中的src目录主要包含以下内容和模块: alloc:内存分配相关函数,比如alloc::boxed::Box、alloc::string::String等。 ascii:ASCII相关工具函数。 char:字符相关类型和函数,如Char、char等。 cmp:比较相关trait和函数,如Ord、Eq、PartialOrd等。 env:环境变量相关功能…

什么是React中的有状态组件(stateful component)和无状态组件(stateless component)?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

微信小程序如何跳转页面

1.wx.navigateTo&#xff1a;用于跳转到其他页面&#xff0c;并保留当前页面。通过该 API 跳转后&#xff0c;可以通过返回按钮回到原页面。 wx.navigateTo({url: /pages/otherPage/otherPage })2.wx.redirectTo&#xff1a;用于跳转到其他页面&#xff0c;并关闭当前页面。通…

探索SOCKS5与SK5代理在现代网络环境中的应用

随着互联网技术的飞速发展&#xff0c;网络安全成为了不容忽视的重要议题。其中&#xff0c;网络代理技术作为一种重要的网络安全手段&#xff0c;以其独特的功能和优势在网络安全领域占据了重要的位置。本文将探讨两种常见的代理技术&#xff1a;SOCKS5代理和SK5代理&#xff…

Linux中关于glibc包导致的服务器死机或者linux命令无法使用的情况

glibc是gnu发布的libc库&#xff0c;即c运行库。glibc是linux系统中最底层的api&#xff0c;几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外&#xff0c;它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX …

量子计算与量子密码(入门级)

量子计算与量子密码 写在最前面一些可能带来的有趣的知识和潜在的收获 1、Introduction导言四个特性不确定性&#xff08;自由意志论&#xff09;Indeterminism不确定性Uncertainty叠加原理(线性)superposition (linearity)纠缠entanglement 虚数的常见基本运算欧拉公式&#x…

本地新建项目如何推到码云上去

1.先在码云上建立一个空仓库&#xff0c;正常步骤就行。建立完成有readme.md. 2.然后本地建立项目文件&#xff0c;正常脚手架搭建VUE\REACT等。记得要项目git init一下。 3.本地改好的内容commit 一下。 4.本地文件与远端仓库建立连接。git remote add origin https://gite…

基于C/C++的UG二次开发流程

文章目录 基于C/C的UG二次开发流程1 环境搭建1.1 新建工程1.2 项目属性设置1.3 添加入口函数并生成dll文件1.4 执行程序1.5 ufsta入口1.5.1 创建程序部署目录结构1.5.2 创建菜单文件1.5.3 设置系统环境变量1.5.4 制作对话框1.5.5 创建代码1.5.6 部署和执行 基于C/C的UG二次开发…

hypercube背景设置为白色,绘制高光谱3D立方体

import scipy pip install wxpython PyOpenGL和Spectral需要本地安装 可参考链接https://blog.csdn.net/qq_43204333/article/details/119837870 参考&#xff1a;https://blog.csdn.net/Tiandailan/article/details/132719745?spm1001.2014.3001.5506Mouse Functions:left-cl…

何判断自己网络是否支持IPV6

环境&#xff1a; Win10专业版 IPV6 问题描述&#xff1a; 何判断自己网络是否支持IPV6 解决方案&#xff1a; 要判断您的网络是否支持 IPv6&#xff0c;可以采取以下方法&#xff1a; 检查您的网络设备&#xff08;如路由器、交换机等&#xff09;是否支持 IPv6。通常&a…