射线投射和反射

本教程讨论射线投射反射,分别通过 physics.rayCast()physics.reflectRay() API 实现。它还附带一个“真实世界”演示项目,可在此处下载 此处

什么是射线投射?

在最基本的层面上,射线投射涉及从一个点到另一个点发射一条射线 —一条直线,用它来检测该路径中是否存在一个或多个 物理实体。 除此之外,这还可以用来检测一个物体是否在枪的射击路径中,敌人的“视线”等等。本质上,这是一种快速有效地查询物理世界中其他物理对象存在的方法。

Corona 还提供了一个方便的 API,用于从射线击中的任何物体反射射线。对于任何注册命中的射线投射,physics.reflectRay() 会返回一个表示反射方向的向量,其大小(长度)为 1。本教程演示了实际使用中的射线投射和射线反射。

镜子房间

如果您还没有下载 演示项目,请下载并跟随代码进行操作。

此演示中的一个重要步骤是配置“世界” —在本例中是一个带有激光炮塔的镜子房间。您可以根据需要设置您的物理世界,但此示例循环遍历一系列包含每个镜子 xyrotation 值的表

-- Set up mirror positions relative to the center of the content area
local mirrorSet = {
    { 0, -125, 90 },    -- top
    { 105, -60, -35 },  -- right-upper
    { 105, 60, 35 },    -- right-lower
    { 0, 125, 90 },     -- bottom
    { -105, -60, 35 },  -- left-upper
    { -105, 60, -35 }   -- left-lower
}

炮塔本身是一个带有径向物理实体的标准显示对象——是的,射线投射检测/反射也适用于径向实体!

-- Create turret
turret = display.newImageRect( mirrorGroup, "turret.png", 48, 48 )
physics.addBody( turret, "dynamic", { radius=18 } )
turret.x, turret.y = display.contentCenterX, display.contentCenterY

接下来,我们通过将炮塔的角速度设置为示例代码开头附近的 turretSpeed 变量来开始旋转炮塔。然后,我们启动一个重复计时器,每 2000 毫秒发射一次激光。

-- Start rotating turret
turret.angularVelocity = turretSpeed

-- Start repeating timer to fire beams
timer.performWithDelay( 2000, fireOnTimer, 0 )

投射射线

投射实际射线很简单。找到 castRay() 函数并检查突出显示的行

local function castRay( startX, startY, endX, endY )

    -- Perform ray cast
    local hits = physics.rayCast( startX, startY, endX, endY, "closest" )

physics.rayCast() 的前四个参数指示xy 起始位置,以及 xy 目标位置。这里,这些值作为 startXstartYendXendY 分别传递给 castRay() 函数。

第五个参数虽然是可选的,但值得注意,因为它指示您希望从射线投射返回的结果“类型”。当前可用以下选项

利用结果

physics.rayCast() API 返回一个描述每次命中的 数组),但由于我们只对射线投射注册的第一次命中感兴趣(在本例中是距离射线起点最近的),我们将只处理数组中的第一个表

local function castRay( startX, startY, endX, endY )

    -- Perform ray cast
    local hits = physics.rayCast( startX, startY, endX, endY, "closest" )

    -- There is a hit; calculate the entire ray sequence (initial ray and reflections)
    if ( hits and beamGroup.numChildren <= maxBeams ) then

        -- Store first hit to variable (just the "closest" hit was requested, so use "hits[1]")
        local hitFirst = hits[1]

        -- Store the hit X and Y position to local variables
        local hitX, hitY = hitFirst.position.x, hitFirst.position.y

从表示第一次命中的表hits[1],设置为 hitFirst,我们得到以下详细信息

使用此信息,我们可以从起点到命中点绘制一组分层线

local function drawBeam( startX, startY, endX, endY )

    -- Draw a series of overlapping lines to represent the beam
    local beam1 = display.newLine( beamGroup, startX, startY, endX, endY )
    beam1.strokeWidth = 2 ; beam1:setStrokeColor( 1, 0.312, 0.157, 1 ) ; beam1.blendMode = "add" ; beam1:toBack()
    local beam2 = display.newLine( beamGroup, startX, startY, endX, endY )
    beam2.strokeWidth = 4 ; beam2:setStrokeColor( 1, 0.312, 0.157, 0.706 ) ; beam2.blendMode = "add" ; beam2:toBack()
    local beam3 = display.newLine( beamGroup, startX, startY, endX, endY )
    beam3.strokeWidth = 6 ; beam3:setStrokeColor( 1, 0.196, 0.157, 0.392 ) ; beam3.blendMode = "add" ; beam3:toBack()
end

请注意,此示例使用了三条宽度不同的线,彼此分层,混合模式"add"。这将创建一个具有更亮中心区域的激光器的良好视觉外观,该中心区域向边缘逐渐淡化为红色/橙色。

反射射线

如果您希望将射线从其击中的表面反射回来,Corona 提供了方便的 physics.reflectRay() API。如前所述,这将返回一个表示反射方向的向量,其大小(长度)为 1

调用此函数需要三个参数

在我们的示例中,physics.reflectRay() 调用如下所示

local reflectX, reflectY = physics.reflectRay( startX, startY, hitFirst )
重要

请注意,from_xfrom_y 参数基于原始射线投射的起始位置startXstartY而不是 endXendY 的命中位置。换句话说,physics.reflectRay() 只处理一次反射,而不是一系列递归反射。为了使激光继续在镜子之间反弹/反射,我们需要每次都进行另一次射线投射,如下所示。

要拉伸反射光线并为其设置目标点,只需考虑您选择的向量长度并将其与命中点相加即可

local reflectX, reflectY = physics.reflectRay( startX, startY, hitFirst )
local reflectLen = 1600
local reflectEndX = ( hitX + ( reflectX * reflectLen ) )
local reflectEndY = ( hitY + ( reflectY * reflectLen ) )
注意

在本例中,我们使用 1600 作为反射向量长度,但可以根据需要进行设置。事实上,这个值有些随意,除非您有特定需要限制反射光线的长度,否则通常最好将此长度设置为远大于其在击中另一个物体之前可能传播的距离的值。

使用此新信息,我们可以再次调用 castRay() 函数(延迟 40 毫秒后)以从命中点到下一个目标点绘制一组新线

local reflectX, reflectY = physics.reflectRay( startX, startY, hitFirst )
local reflectLen = 1600
local reflectEndX = ( hitX + ( reflectX * reflectLen ) )
local reflectEndY = ( hitY + ( reflectY * reflectLen ) )

-- If the ray is reflected, cast another ray
if ( reflectX and reflectY ) then
    timer.performWithDelay( 40, function() castRay( hitX, hitY, reflectEndX, reflectEndY ); end )
end

终止过程

在演示项目中,在 castRay() 函数内,检查以下行

if ( hits and beamGroup.numChildren <= maxBeams ) then

基本上,此条件检查可确保 castRay() 函数仅在没有更多命中达到 maxBeams 值时才重复。这对于在特定光束开始在两个表面之间以类似的模式来回反弹时停止反射过程非常有用,因为它可能会几乎无限期地重复。

当满足任一条件并且过程终止时,我们绘制最后一道光束,然后调用一个基本过渡以淡出父显示组

-- Draw the final beam
drawBeam( startX, startY, endX, endY )

-- Fade out entire beam group after a short delay
transition.to( beamGroup, { time=800, delay=400, alpha=0, onComplete=resetBeams } )

过渡完成后,我们调用 resetBeams() 函数来清除/重置组

local function resetBeams()

    -- Clear all beams/bursts from display
    for i = beamGroup.numChildren,1,-1 do
        local child = beamGroup[i]
        display.remove( child )
        child = nil
    end

    -- Reset beam group alpha
    beamGroup.alpha = 1

    -- Restart turret rotating after firing is finished
    turret.angularVelocity = turretSpeed
end

这基本上完成了演示演练——在一个重复的计时器上,从炮塔进行射线投射并绘制一组激光线。如果射线击中物理世界中的另一个物体,则计算射线的反射并将其拉伸到另一个点。根据该数据,执行另一次射线投射,并且该过程继续进行,直到没有更多命中达到光束阈值为止。

结论

如您所见,射线投射可以成为您物理工具集中有用的补充。从查询物理世界到计算移动物体的潜在路径,射线投射快速、简单,并且只需几行代码即可完成。