第十三章: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(裙文件里面也可以下载所有内容)。
相关文章:
第十三章:L2JMobius学习 – 玩家攻击怪物
本章节,我们学习一下玩家周边怪物的刷新。在上一章节中,我们提过这个事情。当玩家移动完毕之后,会显示周围的游戏对象,其中就包括NPC怪物。当然,玩家“孵化”自己(调用spawnMe方法)的时候&#…...
Module not found: Error: Can‘t resolve ‘core-js/modules/es.promise.js‘
1.遇到的问题 具体错误: ERROR in ./src/js/index.js 1:0-48 产环境配置15js兼容性处理srcjsERROR in ./src/js/index.js 2:0-39 Module not found: Error: Cant resolve core-js/modules/es.promise.js in D:DesktopMy FilesRecentlyStudyWebPackdemo3.webpack生…...
09-React路由使用(React Router 6)
9-React Router 6的使用 1.概述 React Router 以三个不同的包发布到 npm 上,它们分别为: react-router: 路由的核心库,提供了很多的:组件、钩子。react-router-dom: 包含react-router所有内容,并添加一些专门用于 DOM …...
Linux上常用网络相关命令
1. ifconfig: - 显示所有网络接口的配置信息:ifconfig - 显示特定网络接口(例如eth0)的配置信息:ifconfig eth0 2. ip: - 显示网络接口的配置信息:ip addr show - 显示路由表&…...
contenteditable实现文本内容确认提示
功能需求: 列表进行批量查询,需要对输入的值做提交校验,分三种情况: 若部分字符串有误,部分字符串需要变更字体颜色做提示,再次点击确认则对部分正确数据执行批量查询 若全部数据有误则变更字体颜色做提示&…...
vue2vue3--render函数(h)
目录 h函数 方法1. 在Options API中的使用 方法2. 在Composition API中的使用 Vue 2中的渲染函数 基础 vue2 vue3 vue3--声明渲染函数 节点、树以及虚拟 DOM 虚拟 DOM createElement 参数 深入数据对象 约束 vue2 vue3 使用 JavaScript 代替模板功能…...
网络协议--动态选路协议
10.1 引言 在前面各章中,我们讨论了静态选路。在配置接口时,以默认方式生成路由表项(对于直接连接的接口),并通过route命令增加表项(通常从系统自引导程序文件),或是通过ICMP重定向…...
30天精通Nodejs--第一天:入门指南
介绍 看一下下面这段比较官方的介绍: Node.js是一个基于Chrome V8引擎的JavaScript运行时环境,可以用于构建可扩展的网络应用程序。它的特点在于能够使JavaScript在服务器端运行,能够利用JavaScript的强大功能来处理服务器端的事务。 Nodejs的特点 高效的异步编程:Node.…...
C# ref用法,实现引用传递(地址传递)
前言: 今天这篇文章我们简单学习一下C# ref的用法,在看别人的代码不至于看不懂逻辑,虽然这是一个比较简单的知识点,但是还是值得我们去学习一下关于这个知识点一些概念,我们知道在C# 中我们的函数参数,一般…...
微信小程序数据交互------WXS的使用
🎬 艳艳耶✌️:个人主页 🔥 个人专栏 :《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 ,越幸运。 1.数据库连接 数据表结构: 数据测式: 2.后台配置 pom.xml <?xml version&quo…...
【数据结构】String类对象的创建与字符串常量池的“神秘交易”
作者主页:paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文录入于《JAVA数据结构》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力…...
搞个微信小程序002:个人信息
新建一个用于,和001中一样,然后,就改掉两个文件: index.wxml: <view><!-- 头像区域 --><view class"top"><view class"user-img"><image src"/images/tx.png"><…...
.obj模型文件(带材质和纹理)合并的基本思路
1、将v开头的顶点信息依次拷贝到合并新.obj中 2、将vt纹理坐标依次拷贝到合并新.obj中 3、f(面)的合并 步骤: (1)第一个obj文件的f(面)原封不动拷进新.obj中 (2)第二个…...
es : java 查询
1. POM 配置 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.6.2</version></dependency> 2. 建立ES集群连接 RestHighLevelClient cli…...
MySQL MVCC机制探秘:数据一致性与并发处理的完美结合,助你成为数据库高手
一、前言 在分析 MVCC 的原理之前,我们先回顾一下 MySQL 的一些内容以及关于 MVCC 的一些简单介绍。(注:下面没有特别说明默认 MySQL 的引擎为 InnoDB ) 1.1 数据库的并发场景 数据库并发场景有三种,分别是: 读-读…...
5分钟搞懂分布式可观测性
可观测性是大规模分布式(微服务)系统的必要组件,没有可观测系统的支持,监控和调试分布式系统将是一场灾难。本文讨论了可观测系统的主要功能,并基于流行的开源工具搭建了一套可观测系统架构。原文: A Primer on Distributed Systems Observab…...
桥梁结构健康监测系统落地方案
桥梁结构健康监测的意义是多方面的。首先,它可以实时采集桥梁的结构数据,并对其进行处理和分析,以确定结构损伤的位置、评估桥梁的健康状况,并预测承载力的发展趋势。这有助于及时发现桥梁的结构问题和潜在风险,为采取…...
hive和presto的求数组长度函数区别及注意事项
1、任务 获取邮箱字符串’后字符串 ,求长度 2、hive & spark-sql 求数组长度的函数 size hive & spark-sql 求数组长度的函数 sizeselect size(split(email, )),split(email, ),split(email, )[0],split(email, )[1] FROM (select "jack126.com"…...
Kotlin Lambda表达式与标准库中的高阶函数
在Kotlin中,Lambda表达式和标准库中的高阶函数为我们提供了一种简洁而强大的方式来处理集合和执行各种操作。本篇博客将介绍Lambda表达式的基本概念,并结合标准库中的高阶函数示例,展示它们的用法和功能。 Lambda表达式的基本概念 Lambda表…...
【JavaEE初阶】 CAS详解
文章目录 🌲什么是 CAS🚩CAS伪代码 🎋CAS 是怎么实现的🌳CAS的应用🚩实现原子类🚩实现自旋锁 🎄CAS 的 ABA 问题🚩什么是 ABA 问题🚩ABA 问题引来的 BUG🚩解决…...
JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...
springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
系统掌握PyTorch:图解张量、Autograd、DataLoader、nn.Module与实战模型
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文通过代码驱动的方式,系统讲解PyTorch核心概念和实战技巧,涵盖张量操作、自动微分、数据加载、模型构建和训练全流程&#…...
热门Chrome扩展程序存在明文传输风险,用户隐私安全受威胁
赛门铁克威胁猎手团队最新报告披露,数款拥有数百万活跃用户的Chrome扩展程序正在通过未加密的HTTP连接静默泄露用户敏感数据,严重威胁用户隐私安全。 知名扩展程序存在明文传输风险 尽管宣称提供安全浏览、数据分析或便捷界面等功能,但SEMR…...
《信号与系统》第 6 章 信号与系统的时域和频域特性
目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...
车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...
