第十三章: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🚩解决…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...
springboot 日志类切面,接口成功记录日志,失败不记录
springboot 日志类切面,接口成功记录日志,失败不记录 自定义一个注解方法 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;/***…...
前端开发者常用网站
Can I use网站:一个查询网页技术兼容性的网站 一个查询网页技术兼容性的网站Can I use:Can I use... Support tables for HTML5, CSS3, etc (查询浏览器对HTML5的支持情况) 权威网站:MDN JavaScript权威网站:JavaScript | MDN...
