如何在Reworld里制作一个伤害技能

532 阅读9分钟

功能效果展示

运行环境

Win7,Win8,Win10

Reworld版本 体验版

vc_redist.x64 运行环境

针对零基础读者的补充

下载安装 Reworld对应版本

Reworld官网链接:www.reworlder.com/

Reworld创作者之家论坛:bbs.reworlder.com

下载后安装后注册账号打开空地图

思路分析

一个完整的技能包括技能操作按钮、技能CD、技能释放、技能特效、角色释放动作、技能伤害。 技能释放采用手游常用的按钮点击释放的方式,通过点击技能按钮开始释放技能。 技能CD限制技能释放,因此限制技能按钮点击就可以达成效果,CD展示效果用UI控件的图片填充比例制作。 技能特效和技能动作使用重启世界的特效功能和动画编辑器可以自由制作。 技能伤害上,这个技能设计为范围型的伤害,可以采用重启世界的范围检测函数获取范围内的角色,通过遍历这些角色进行生命值的计算。

技能按钮

技能释放通过按钮点击触发,首先要制作一个释放按钮。在界面初始化创建2D界面容器,然后在容器中创建按钮控件,调整容器和按钮控件的大小和位置后在按钮控件的图片资源中导入准备好的图片。创建后的效果如下图所示:

按钮的释放是通过客户端的按钮,但是技能的美术和伤害效果都需要同步给其他玩家,因此释放后的技能逻辑需要在服务器处理,那么就需要使用事件对象做技能释放和逻辑处理的通信。 在工作区添加了一个事件对象,并更名为SkillEvent。 在按钮控件下创建客户端脚本,用于编写按钮释放的功能。点击操作使用按钮控件的OnClick()事件触发,点击按钮后使用事件对象的FireServer(…)发送释放消息到服务器中,点击技能按钮的代码如下

--获取技能事件对象
local skillEvent = WorkSpace.SkillEvent
--技能按钮点击事件
script.Parent.OnClick:Connect(function()
	--发送技能释放消息
	skillEvent:FireServer()	
end)

技能冷却

考虑到技能需要冷却时间,冷却的视觉表现通过在原有UI基础上加一个图像控件,使用图像的填充属性实现。 在按钮控件下创建一个图像控件,调整锚点使它完全遮住按钮。在图像控件的图片资源中设置一个图片。

图像填充方式选择Radial360度的方式,填充起点根据初始效果设定,这里设置为2,初始的填充比例为0。如下图所示:

技能冷却的表现出现在点击技能释放按钮之后,冷却的功能代码也写在技能释放的脚本中,在按钮点击后执行。冷却期间按钮不能再次点击,通过使按钮失效实现,冷却的UI效果使用循环结构修改图像控件填充比例,并结合时间实现。 在客户端脚本继续编写上述逻辑的代码: --获取技能事件对象

local skillEvent = WorkSpace.SkillEvent
--获取冷却图像控件
local cd = script.Parent.图像控件
--技能按钮点击
script.Parent.OnClick:Connect(function()
	--发送消息
	skillEvent:FireServer()	
	--设置按钮不生效
	script.Parent.Interactable = false
	--显示冷却图片
	cd.IsVisable = true
	--循环更改填充比例
	for i = 0,1,0.01 do
		cd.FillAmount = i
		wait(0.01)
	end
	--冷却结束设置按钮生效
	script.Parent.Interactable = true
	--初始化图片填充比例
	cd.FillAmount = 0
end)

客户端做的事很简单,就是告诉服务器玩家点击这个技能按钮了,并且做了一个冷却的表现。

效果美术

这个技能包括三个视觉效果,分别是技能特效、释放动作和是一个技能武器,这三种素材都可以在编辑器内制作,分别使用特效对象制作特效,动画编辑器制作动作,使用工具制作技能武器。这里不介绍详细的制作过程,可通过文章文档、文档和文档做进一步了解。 制作的成品素材如下所示:

创建动作对象将动作保存在场景中。

这些美术素材在制作后,都放在通用存储中便于随时反复的调用。

技能初始化

在玩家进入游戏时,技能展示的动作、特效和装备的武器需要配置到角色身体上,这里使用玩家加载事件PlayerAdded()和角色加载事件AvatarAdded()做处理。工具只需指定父级为角色即可装备,动作对象也需要放在角色下才能播放。 在服务器逻辑建立服务器脚本,并更名技能脚本。

书写素材初始化的代码:

--注册玩家进入游戏事件
Players.PlayerAdded:Connect(function(id)
	--获取玩家
	local player = Players:GetPlayerByUserId(id)
	--注册角色加载事件
	player.AvatarAdded:Connect(function(ava)
		--等待一秒,为了保证角色对象实体化
		wait(1)
		--武器放在角色下
		CommonStorage.法杖:Clone(ava)
		--动作放在角色下
		CommonStorage.法杖动作:Clone(ava)
		--特效放在角色的身体部位下
		for k,v in pairs(CommonStorage.effect:GetAllChild()) do
			v:Clone(ava.胸)
		end
	end)
end)

技能释放

技能的释放效果包括特效的播放、角色动作和对周围的角色造成伤害。特效和动作的播放都通过播放属性的修改实现,而造成伤害则是通过范围检测的函数获取周围的目标然后扣除角色的生命值。这部分的逻辑在玩家点击技能按钮后,在服务器执行。 上文中技能按钮部分,我们已经完成了玩家点击技能按钮的功能,这个功能将释放技能的消息发送到了服务器。那么接下里就是在服务器编写技能的具体释放逻辑。 技能的设计要考虑到想达到可拓展的效果的编辑参数了,在后面需要其他技能时,只需调整参数就可以做一个新的技能。这里设计的特效粒子位置,多段伤害,伤害波及范围,还有范围检测需要的必要参数忽略物体的表。

--定义技能事件对象
local skillEvent = WorkSpace.SkillEvent
--[[
	effectPos:粒子位置
(伤害中心点)
	someDemage:几段伤害
	distance:波及范围
(伤害半径)
	ignoreTable:要忽略的物体表
]]
--技能主方法
function EffectEnter(id,someDemage,distance,ignoreTable)
	--获取点击按钮玩家
	local player = Players:GetPlayerByUserId(id)
	--设置伤害中心点
	local effectPos = player.Avatar.Position
	--距离取绝对值
	distance=math.abs(distance)
	--定义伤害间隔
	local DamageInterval=0.1
	--定义范围检测起始点和终止点
	local startPos=Vector3.New(effectPos.x-distance/2,effectPos.y-distance/2,effectPos.z-distance/2)
	local endPos=Vector3.New(effectPos.x+distance/2,effectPos.y+distance/2,effectPos.z+distance/2)
	--使人物不能移动
	local oldmovespeed = player.Avatar.MoveSpeed
	player.Avatar.MoveSpeed = 0
	--播放动作
	player.Avatar.法杖动作.Play = true
	--开启粒子特效
	for k,v in pairs(player.Avatar.胸:GetAllChild()) do
		v.Enable=true
	end
	--初次释放先进行一次伤害计算,及时性
	Damage(startPos,endPos,ignoreTable,0,player.PlayerId)
	--根据多段数多次进行伤害计算
	for i=1,someDemage-1, 1 do
		Damage(startPos,endPos,ignoreTable,DamageInterval,player.PlayerId)
	end
	--释放完关闭粒子
	for k,v in pairs(player.Avatar.胸:GetAllChild()) do
		v.Enable=false
	end
	--关闭动作
	player.Avatar.法杖动作.Play = false
	--恢复人物移动
	player.Avatar.MoveSpeed = oldmovespeed
end
--接收技能事件
skillEvent.ServerEventCallBack:Connect(EffectEnter)

将可变参数放在客户端,在多技能时,每个技能都由一个技能按钮操控,这样安排便于根据技能按钮操作来改变参数。所以在之前的客户端代码中补上参数传递:

--[[
	someDemage:几段伤害
	distance:波及范围
	ignoreTable:要忽略的物体表
]]
local	someDemage = 10
local	distance = 3
local	ignoreTable = {}
--获取本地玩家
local player = Players:GetLocalPlayer()
--定义技能事件对象
local skillEvent = WorkSpace.SkillEvent
--定义图像控件
local cd = script.Parent.图像控件
--按钮点击事件注册
script.Parent.OnClick:Connect(function()
	--发送消息
	skillEvent:FireServer(someDemage,distance,ignoreTable)	
	--设置按钮不生效
	script.Parent.Interactable = false
	--初始化图片填充比例
	cd.FillAmount = 0
	--循环更改填充比例
	for i = 1,100 do
		cd.FillAmount = cd.FillAmount + 0.01
		wait(0.01)
	end
	--设置按钮生效
	script.Parent.Interactable = true
end)

技能伤害

上边的服务器代码中。35行使用了一个自定义Damage()方法,这个方法就是用于伤害计算的。直接上代码,在注释中深入了解下:

--[[
	startPos;技能范围起始点
	endPos:技能范围终止点
	ignoreTable:忽略表
	DamageInterval:多段伤害间隔
	id:自己玩家id
]]
--伤害计算方法
function Damage(startPos,endPos,ignoreTable,DamageInterval,id)
	--伤害计算的间隔
	wait(DamageInterval)
	local list={}
	--获取范围内全部物角色
	list=filtrateObj(startPos,endPos,ignoreTable,list)
	--判断是否为空表
	if next(list)==nil then return end
	--遍历表
	for k,v in pairs(list) do
		--如果角色的id不是自己的id
		if v.PlayerId~=id then
			--单次伤害
			v:TakeDamage(10)
		end
	end
end

伤害目标获取

范围伤害的表现方式可以有很多种,但核心思想一般是以一个基准点来划出一片区域,在区域中的玩家或者怪物受到伤害,可以采用FindPartsInZoneWithIgnoreList(minPoint,maxPoint,ignoreTable,maxParts)函数做这个计算。 在上文的服务器脚本中使用了一个自定义方法filtrateObj(),这个方法是做范围检测的方法,定义这个方法的原因是范围检测会检测范围内所有的符合参数maxParts数量的对象。而一旦范围内的对象大于这个值,就会有一些对象被忽略掉。而角色被检测的时候,角色内的全部对象都会被单独检测,大大占用检测数量。所以写了这个方法,通过递归算法,剔除掉角色下边的全部对象,只保留角色对象用于计算伤害。

--选择优化脚本,因为范围检测会把角色身体部件也算作单位,所以要进行优化,部位零件剔除,保留角色本身
function filtrateObj(startPos,endPos,ignoreTable,list)
	--忽略检测表
	ignoreTable=(type(ignoreTable)~="table") and {} or ignoreTable
	--判断是否剔除完成
	local filtrateBool=true
	--判断表不为空
	if next(list)~=nil then
		for k,v in pairs(list) do
			--筛选,如果有未剔除的非角色的部件,那么剔除未成功
			if v:IsClass("Avatar") == false then
				filtrateBool=false
				break
			end
		end
		--递归终止
		if filtrateBool then
			return list
		end
	end
	--定义最大检测数
	local maxParts=100
	--范围检测
	local filtratelist=WorkSpace:FindPartsInZoneWithIgnoreList(startPos,endPos,ignoreTable,maxParts)
	local num=0
	for k,v in pairs(filtratelist) do
		if v:GetAncestorByClassName("Avatar")~=nil then
			--根据物体guid记录储存角色本身,剔除角色身体部件
			table.insert(ignoreTable,v:GetAncestorByClassName("Avatar"))
			list[v:GetAncestorByClassName("Avatar").rw___entityGuid]=v:GetAncestorByClassName("Avatar")
			num=num+1
		end
	end
	--设置递归算法
	return num>0 and filtrateObj(startPos,endPos,ignoreTable,list) or {}
end