中tile函数_基于 tile 带滑动优化的平滑移动算法

7b566322f0640f98f0964f55eabb9d86.png

综述

移动行为是游戏中的一个基本概念,其最简形式可表达为向一个位置值上累加步进值。本文描述一种带容差滑动的平滑移动算法,并且可以适配至多种基于 tile 的场景。

目标

记得炸弹人的平滑移动手感吗?

59e7813779f8591e292c7159f624e930.png
炸弹人

当按下右方向键但是你的炸弹人脚边有块格子阻挡时,角色会先尝试在另一坐标轴向其(相对于阻挡格子)较易通行的方向移动一小段距离,直到不再被阻挡再继续向期望方向移动。

c412a7a93e400c01ceadc5f528e9474c.png
按右键

本文以此手感为目标,并实现一套可适用于不同角色、tile 大小的解决方案。

逐网格移动

从最简单的移动行为开始,按惯例 tile 地图以连续整型代表其索引:

2b5f0230e48dff3031a2a495c0f0b77c.png
Tile 地图

用两个整型数分别代表 x,y 坐标。则移动和渲染代码为:

local pos = Vec2.new(0, 0)
local hero = Resources.load('hero.spr')
local ground = Resources.load('ground.map')

function update()
  -- Grid-wise move.
  if keyp(KeyCode.Left) then
    pos.x = pos.x - 1
  elseif keyp(KeyCode.Right) then
    pos.x = pos.x + 1
  end
  if keyp(KeyCode.Up) then
    pos.y = pos.y - 1
  elseif keyp(KeyCode.Down) then
    pos.y = pos.y + 1
  end

  -- Pixelated rendering.
  map(ground, 0, 0)
  spr(hero, pos.x * TILE_SIZE, pos.y * TILE_SIZE)
end

在这种情况下,对象位置和 tile 索引使用同一套格子单位。

23377ad5b7149647cb8be1e77194d898.gif
逐网格

逐像素移动

逐像素移动可以通过简单修改逐格子移动版本的最后一行 sprite 渲染得到:

local pos = Vec2.new(0, 0)
local hero = Resources.load('hero.spr')
local ground = Resources.load('ground.map')

function update()
  -- Pixelated move.
  if key(KeyCode.Left) then
    pos.x = pos.x - 1
  elseif key(KeyCode.Right) then
    pos.x = pos.x + 1
  end
  if key(KeyCode.Up) then
    pos.y = pos.y - 1
  elseif key(KeyCode.Down) then
    pos.y = pos.y + 1
  end

  -- Pixelated rendering.
  map(ground, 0, 0)
  spr(hero, pos.x, pos.y)
end

此时对象坐标以像素为单位,与渲染相同。表现上相较上一个版本更加平滑。

0ff23e835bee26f8883446bdf2bc9cc2.gif
逐像素

碰撞检测

在本文的理想化伪代码中,我们并未考量任何时间相关量,但为什么逐像素移动代码比逐网格移动代码看起来更简单些呢?不妨考虑添加与 tile 的碰撞检测来判断是否可移动,则代码为:

-- Grid-stepped.
function isMovable()
  local index = pos + expDir
  local tile = mget(ground, index.x, index.y)
  local movable = not isBlock(tile)

  return movable
end

在逐像素移动中我们需要在访问地图 tile 之前先进行一个从像素到网格的坐标转换:

-- Pixel-stepped.
function isMovable()
  local newPos = pos + expDir
  local index = Vec2.new(
    math.floor(newPos.x / TILE_SIZE),
    math.floor(newPos.y / TILE_SIZE)
  )
  local tile = mget(ground, index.x, index.y)
  local movable = not isBlock(tile)

  return movable
end

从这里开始代码复杂度会逐渐增加,但不必担心最终代码过于复杂。

位置坐标不再截断到网格单位。我们现在只能检查角色前进方向边沿所有可能碰到的 tile 的可通过与否,才能最终确定角色是否可以移动。

a2de775585e00b0b2e99612edda6906b.gif
多扫描线

不妨称待检查的路径为“扫描线”。在真实项目中,扫描线越多则程序的 CPU 资源占用越高。由于 tile 的大小通常不至于小到只有 1x1 像素,在工程上这一求解过程可以简化为若干段带间距的扫描线,而不必逐像素计算。

local STEP_COUNT = 3

function isVerticallyMovable()
  local step = CHAR_SIZE / STEP_COUNT
  local beginX = pos.x + expDir.x
  local endX = pos.x + expDir.x + CHAR_SIZE
  local movable = true
  for x = beginX, endX, step do
    local newPos = pos + Vec2.new(x, pos.y + expDir.y)
    local index = Vec2.new(
      math.floor(newPos.x / TILE_SIZE),
      math.floor(newPos.y / TILE_SIZE)
    )
    local tile = mget(ground, index.x, index.y)
    if isBlock(tile) then
      movable = false
      break
    end
  end

  return movable
end

做为额外的好处,采用扫描线的本算法亦适用于不同角色和 tile 大小。

5711985a1e808cd9ac5db0793f567492.png
小型角色

8fec421e1ae3f662bfc38753f2d28f3e.png
大型角色

滑动优化

在面对一条很窄的通路时,你仍然需要小心翼翼的将角色和地图元素对齐才能通过。为优化这一行为,首先将扫描线束的宽度略微缩小用以容差。

4b7e87a9890c3f07f67fcde22324e9de.gif
缩小容差

其次计算表示其畅通程度的通过率。

5bc4bfed54e7c97752090b214413e3bc.gif
通过率

用畅通的扫描线数量除以总数得到通过率:

local SHRINK_SIZE = 1
local MOVABLE_RATE = 0.6667
local STEP_COUNT = 3

function isVerticallyMovable()
  local step = (CHAR_SIZE - SHRINK_SIZE * 2) / STEP_COUNT
  local beginX = pos.x + expDir.x + SHRINK_SIZE
  local endX = pos.x + expDir.x + CHAR_SIZE - SHRINK_SIZE
  local passed = 0
  local total = 0
  for x = beginX, endX, step do
    local newPos = pos + Vec2.new(x, pos.y + expDir.y)
    local index = Vec2.new(
      math.floor(newPos.x / TILE_SIZE),
      math.floor(newPos.y / TILE_SIZE)
    )
    local tile = mget(ground, index.x, index.y)
    if not isBlock(tile) then
      passed = passed + 1
    end
    total = total + 1
  end
  local movable = passed / total >= MOVABLE_RATE

  return movable
end

最后,如果通过率大到足以移动但仍有部分阻挡,我们在另一坐标轴上进行微小滑动,然后继续向行走方向移动。

4ef5fba0f47dbf1a724b3efc7e546744.gif
平滑移动

单向平台

在某些游戏中你可以跳上某一平台,然后平台会支撑着角色防止下落;或你控制角色通过一扇门,但无法原路返回……诸如此类,我们称其为单向平台或单向门。通过添加一个掩码参数以控制通行条件,我们得以实现这一行为。

local NONE, LEFT, RIGHT, UP, DOWN = 0, 1, 2, 4, 8
...

function isVerticallyMovable(passMask)
  ...
  for x = beginX, endX, step do
    ...
    local mask = expDir.y < 0 and UP or DOWN
    if not isBlock(tile) or (passMask & mask ~= NONE) then
      passed = passed + 1
    end
    total = total + 1
  end
  local movable = passed / total >= MOVABLE_RATE

  return movable
end

通过向函数传入 UP 现在可以由下至上单向通过此平台。

541509be87097961bb429b1cbc21140c.gif
单向平台

应用场景

此算法非常适合顶视和横版平台跳跃游戏。通过适当构造场景结构,这一思路也能用于部分 3D 场景。

6195c0cac9b19ae2df8207297ca650a8.gif
顶视

44757af3b85918d5d80045b9067f93b3.gif
平台

本算法已内置于 Bitty Engine 和 BASIC8。

Bitty Engine​store.steampowered.com BASIC8 on Steam​store.steampowered.com
81960f1b0f5c74fc60b5fcbb76ded543.png

已知问题

穿透问题

本算法未做连续性检查。如果移动过程单步步长过大,则会造成穿透问题。可以把较大的步长分解成几段小的步骤规避此问题。

实数精度

IEEE-754 标准下的浮点数精度有限。当尝试在某一大地图边缘移动一小段距离时会出现精度问题。表现为无法准确向一个较大数值上加减另一个较小数值。使用双精度浮点数代替单精度能缓解此问题,但也只是把问题显现的时机推迟到更大的数值上了。一个可行的方案是给所有物体添加偏移量,使角色、地图、摄像机等在任意时刻的坐标都被投影在一个可精确计算的范围内。

实现代码

点击查看完整 C++ 代码

Smooth Tile-based Movement Algorithm with Sliding​paladin-t.github.io
0e4293d9b430418ad347f27e121192bd.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值