'use strict'
// ******************************** 场景对象 ********************************
const Scene = new class {
/** 当前绑定场景上下文
* @type {SceneContext|null}
*/ binding = null
/** 绑定场景指针(0或1)
* @type {number}
*/ pointer = 0
/** 场景缩放系数
* @type {number}
*/ scale = 1
/** 场景上下文列表(A、B场景)
* @type {Array<SceneContext|null>}
*/ contexts = [null, null]
/** 默认场景上下文(空场景)
* @type {SceneContext}
*/ default
/** 当前场景的ID->场景对象映射表
* @type {Object}
*/ idMap
/** 当前场景的视差图管理器
* @type {SceneParallaxManager}
*/ parallaxes
/** 当前场景的角色管理器
* @type {SceneActorList}
*/ actors
/** 当前场景的动画管理器
* @type {SceneAnimationList}
*/ animations
/** 当前场景的触发器管理器
* @type {SceneTriggerList}
*/ triggers
/** 当前场景的区域管理器
* @type {SceneRegionList}
*/ regions
/** 当前场景的光源管理器
* @type {SceneLightManager}
*/ lights
/** 当前场景的粒子发射器管理器
* @type {SceneParticleEmitterList}
*/ emitters
/** 当前场景中可见的角色列表
* @type {Array<Actor|null>}
*/ visibleActors = []
/** 当前场景中可见的动画列表
* @type {Array<Animation|null>}
*/ visibleAnimations = []
/** 当前场景中可见的触发器列表
* @type {Array<Trigger|null>}
*/ visibleTriggers = []
/** 当前场景中可见的粒子发射器列表
* @type {Array<Trigger|null>}
*/ visibleEmitters = []
/** 场景精灵渲染器
* @type {SceneSpriteRenderer}
*/ spriteRenderer
/** 场景直射光渲染器
* @type {SceneSpriteRenderer}
*/ directLightRenderer
/** 场景是否暂停(累计次数)
* @type {number}
*/ paused = 0
/**
* 是否阻止场景输入事件(累计次数)
* @type {number}
*/ preventInputEvents = 0
/**
* 场景粒子数量计数
* @type {number}
*/ particleCount = 0
/** 场景共享坐标点 */
sharedPoint = {x: 0, y: 0}
/** 场景事件侦听器列表 */
listeners = {
initialize: [],
load: [],
create: [],
destroy: [],
keydown: [],
keyup: [],
mousedown: [],
mouseup: [],
mousemove: [],
doubleclick: [],
wheel: [],
gamepadbuttonpress: [],
gamepadbuttonrelease: [],
gamepadleftstickchange: [],
gamepadrightstickchange: [],
}
/** 滤镜模块列表 */
filters = new ModuleList()
/** 初始化场景管理器 */
initialize() {
// 设置初始场景缩放系数
this.setScale(Data.globalData.sceneScale)
// 创建精灵渲染器
this.spriteRenderer = new SceneSpriteRenderer(
this.visibleActors,
this.visibleAnimations,
this.visibleTriggers,
this.visibleEmitters,
)
// 创建直射光渲染器
this.directLightRenderer = new SceneDirectLightRenderer()
// 创建默认场景(空场景)
this.default = new SceneContext('')
// 绑定默认场景
this.bind(null)
// 侦听事件
Input.on('keydown', () => Scene.emitInputEvent('keydown'))
Input.on('keyup', () => Scene.emitInputEvent('keyup'))
Input.on('mousedown', () => Scene.emitInputEvent('mousedown'))
Input.on('mouseup', () => Scene.emitInputEvent('mouseup'))
Input.on('mousemove', () => Scene.emitInputEvent('mousemove'))
Input.on('doubleclick', () => Scene.emitInputEvent('doubleclick'))
Input.on('wheel', () => Scene.emitInputEvent('wheel'))
Input.on('gamepadbuttonpress', () => Scene.emitInputEvent('gamepadbuttonpress'))
Input.on('gamepadbuttonrelease', () => Scene.emitInputEvent('gamepadbuttonrelease'))
Input.on('gamepadleftstickchange', () => Scene.emitInputEvent('gamepadleftstickchange'))
Input.on('gamepadrightstickchange', () => Scene.emitInputEvent('gamepadrightstickchange'))
}
// 设置缩放系数
setScale(value) {
this.scale = value
Camera.updateZoom()
}
/** 重置所有场景 */
reset() {
const {contexts} = this
const {length} = contexts
for (let i = 0; i < length; i++) {
if (contexts[i] !== null) {
contexts[i].destroy()
contexts[i] = null
}
}
this.pointer = 0
this.bind(null)
this.spriteRenderer.reset()
this.paused = 0
this.preventInputEvents = 0
}
/** 暂停场景活动 */
pause() {
this.paused++
}
/** 继续场景活动 */
continue() {
this.paused = Math.max(this.paused - 1, 0)
}
/** 阻止场景输入 */
preventInput() {
this.preventInputEvents++
}
/** 恢复场景输入 */
restoreInput() {
this.preventInputEvents = Math.max(this.preventInputEvents - 1, 0)
}
/**
* 激活场景上下文
* @param {number} pointer 场景指针(0或1)
*/
async activate(pointer) {
// 推迟到栈尾执行
await void 0
this.pointer = pointer
const scene = this.get()
if (this.binding !== scene) {
this.bind(scene)
}
}
/**
* 加载场景
* @param {string} id 场景文件ID
* @returns {Promise<SceneContext>}
*/
async load(id) {
// 推迟到栈尾执行
await void 0
// 销毁当前场景上下文
const current = this.get()
if (current !== null) {
current.destroy()
}
// 创建新的场景上下文
const scene = new SceneContext(id)
scene.load()
this.bind(this.set(scene))
return scene.promise
}
/** 删除当前场景 */
async delete() {
// 推迟到栈尾执行
await void 0
const scene = this.get()
if (scene !== null) {
scene.destroy()
this.bind(this.set(null))
}
}
/**
* 更新场景
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
if (Scene.paused === 0) {
Scene.particleCount = 0
this.binding?.update(deltaTime)
}
}
/** 渲染场景 */
render() {
this.binding?.render()
this.filters.render()
}
/**
* 获取当前场景上下文
* @returns {SceneContext|null}
*/
get() {
return this.contexts[this.pointer]
}
/**
* 设置当前场景上下文
* @param {SceneContext|null} scene 场景上下文对象
* @returns {SceneContext|null}
*/
set(scene) {
return this.contexts[this.pointer] = scene
}
/**
* 绑定场景上下文
* @param {SceneContext|null} scene 场景上下文对象
*/
bind(scene) {
if (this.binding) {
this.binding.enabled = false
}
this.binding = scene
if (scene === null) {
scene = this.default
}
// 获取场景组件和方法
this.idMap = scene.idMap
this.parallaxes = scene.parallaxes
this.actors = scene.actors
this.animations = scene.animations
this.triggers = scene.triggers
this.regions = scene.regions
this.lights = scene.lights
this.emitters = scene.emitters
this.convert = scene.convert
this.convert2f = scene.convert2f
this.spriteRenderer.setObjectLists(
scene.actors,
scene.animations,
scene.triggers,
scene.emitters,
)
// 等待加载完毕后更新场景方法和参数
scene.promise?.then(() => {
if (this.binding === scene) {
this.convert = scene.convert
this.convert2f = scene.convert2f
GL.setAmbientLight(scene.ambient)
scene.initialize()
scene.enabled = true
}
})
}
/**
* 添加场景事件侦听器
* @param {string} type 场景事件类型
* @param {function} listener 回调函数
* @param {boolean} [priority = false] 是否将该事件设为最高优先级
*/
on(type, listener, priority = false) {
const list = this.listeners[type]
if (!list.includes(listener)) {
if (priority) {
list.unshift(listener)
} else {
list.push(listener)
}
}
}
/**
* 移除场景事件侦听器(未使用)
* @param {string} type 场景事件类型
* @param {function} listener 回调函数
*/
off(type, listener) {
const group = this.listeners[type]
const index = group.indexOf(listener)
if (index !== -1) {
const replacer = () => {}
group[index] = replacer
Callback.push(() => {
group.remove(replacer)
})
}
}
/**
* 发送场景事件
* @param {string} type 场景事件类型
* @param {any} parameter 场景事件传递参数
*/
emit(type, parameter) {
for (const listener of this.listeners[type]) {
listener(parameter)
}
}
/**
* 发送场景输入事件
* @param {string} type 场景事件类型
* @param {any} parameter 场景事件传递参数
*/
emitInputEvent(type, parameter) {
for (const listener of this.listeners[type]) {
// 每次调用侦听器时判断一下,可能调用后发生变化
if (Scene.paused === 0 && Scene.preventInputEvents === 0) {
listener(parameter)
}
}
}
/**
* 测试场景对象初始化条件
* @param {Object} node 场景对象预设数据
* @returns {boolean}
*/
testConditions(node) {
// 如果场景对象未启用,则不通过
if (node.enabled === false) return false
for (const condition of node.conditions) {
const type = condition.type
const tester = Scene.objectCondTesters[condition.operation]
const getter = Scene.objectCondVarGetters[type]
const value = type[0] === 'g'
? getter(Variable.map, condition.key)
: getter(SelfVariable.map, node.presetId)
// 如果有一个条件不满足,则不通过
if (tester(value, condition.value) === false) {
return false
}
}
return true
}
// 对象条件测试器
objectCondTesters = {
'equal': (a, b) => a === b,
'unequal': (a, b) => a !== b,
'greater-or-equal': (a, b) => a >= b,
'less-or-equal': (a, b) => a <= b,
'greater': (a, b) => a > b,
'less': (a, b) => a < b,
}
// 对象条件变量访问器
objectCondVarGetters = {
'global-boolean': Attribute.BOOLEAN_GET,
'global-number': Attribute.NUMBER_GET,
'global-string': Attribute.STRING_GET,
'self-boolean': Attribute.BOOLEAN_GET,
'self-number': Attribute.NUMBER_GET,
'self-string': Attribute.STRING_GET,
}
/**
* 获取视差图锚点
* @param {SceneParallax|SceneTilemap} parallax 视差图或瓦片地图对象
* @returns {{x: number, y: number}}
*/
getParallaxAnchor(parallax) {
const point = this.sharedPoint
const scene = this.binding
const tw = scene.tileWidth
const th = scene.tileHeight
const cx = Camera.scrollCenterX
const cy = Camera.scrollCenterY
const px = parallax.x * tw
const py = parallax.y * th
const fx = parallax.parallaxFactorX
const fy = parallax.parallaxFactorY
point.x = cx + fx * (px - cx)
point.y = cy + fy * (py - cy)
return point
}
/** 保存场景数据 */
saveData() {
const contexts = []
const active = this.pointer
for (const scene of this.contexts) {
if (scene) {
contexts.push(scene.saveData())
}
}
return {active, contexts}
}
/**
* 加载场景数据
* @param {Object} data
*/
loadData(data) {
this.reset()
this.contexts = [null, null]
for (const context of data.contexts) {
const {id, index} = context
const scene = new SceneContext(id)
scene.loadData(context)
this.contexts[index] = scene
}
// 重新激活场景
this.activate(data.active)
}
}
// ******************************** 场景路径导航器 ********************************
const ScenePathFinder = new class {
/** 顶点开关状态列表 */
states = new Uint8Array(GL.zeros.buffer, 0, 512 * 512)
/** 存放已打开顶点的状态索引的列表 */
indices = new Uint32Array(GL.arrays[1].uint32.buffer, 0, 512 * 512)
/** 存放寻路顶点数据的列表 */
vertices = new Float64Array(GL.arrays[0].float64.buffer, 0, 512 * 512 * 6)
/** 存放已打开顶点的数据索引的列表 */
openset = new Uint32Array(GL.zeros.buffer, 512 * 512, 512 * 512 * 2 / 4)
/** 根据期望值获取优先处理的顶点列表 */
queue = new Uint32Array(GL.arrays[2].uint32.buffer, 0, 100)
/** 相邻图块的八个方向坐标偏移值列表 */
offsets = new Int32Array([0, -1, 1, 0, 0, 1, -1, 0, -1, -1, 1, -1, 1, 1, -1, 1])
/** 已打开顶点的数量 */
stateCount = 0
/** 顶点开集索引列表中的开启数量 */
opensetCount = 0
/** 当前优先队列中的顶点数量 */
queueCount = 0
/** 寻路期望值的阈值,当达到阈值后强制结束寻路 */
threshold = 0
/** 路径的最大额外成本 */
maxExtraCost = 80
/** 地形障碍物的成本 */
terrainObstacleCost = 40
/** 角色障碍物的成本 */
actorObstacleCost = 16
/** 当前场景地图宽度 */
width = 0
/** 当前场景地图高度 */
height = 0
/**
* 当前场景地形数据
* @type {SceneTerrainArray|null}
*/
terrains = null
/**
* 当前场景障碍数据
* @type {SceneObstacleArray|null}
*/
obstacles = null
/**
* 创建路径(Lazy Theta*寻路算法)
* @param {number} startX 起点位置X
* @param {number} startY 起点位置Y
* @param {number} destX 终点位置X
* @param {number} destY 终点位置Y
* @param {number} passage 角色通行区域
* @param {boolean} bypass 是否绕过角色
* @returns {Float64Array} 角色移动路径
*/
createPath(startX, startY, destX, destY, passage, bypass) {
const scene = Scene.binding
const sx = Math.floor(startX)
const sy = Math.floor(startY)
const dx = Math.floor(destX)
const dy = Math.floor(destY)
const width = ScenePathFinder.width = scene.width
const height = ScenePathFinder.height = scene.height
// 如果起点和终点在同一网格,或不受地形限制,或处于场景网格之外,返回单位路径
if (sx === dx && sy === dy || passage === -1 ||
sx < 0 || sx >= width || sy < 0 || sy >= height ||
dx < 0 || dx >= width || dy < 0 || dy >= height) {
return ScenePathFinder.createUnitPath(destX, destY)
}
// 设置终点权重(权重越高,寻路计算步骤越少,计算出来的可能不是最佳路线)
const H_WEIGHT = 1.25
const startIndex = (sx + sy * 512) * 6
const {vertices, openset, queue, offsets} = ScenePathFinder
// 设置绕开角色开关
ScenePathFinder.setBypass(bypass)
// 获取场景地形
ScenePathFinder.terrains = scene.terrainObstacles
// 获取场景障碍
ScenePathFinder.obstacles = scene.obstacles
// 设置寻路阈值,期望值达到阈值后放弃计算
ScenePathFinder.threshold = Math.dist(sx, sy, dx, dy) + ScenePathFinder.maxExtraCost
// 打开起点
ScenePathFinder.openVertex(sx, sy, 0, 0, startIndex, false)
// 循环更新顶点队列
while (ScenePathFinder.updateQueue()) {
for (let i = 0; i < ScenePathFinder.queueCount; i++) {
// 获取队列中的顶点数据,再将其关闭
const oi = queue[i]
const vi = openset[oi] - 1
const tx = vertices[vi ]
const ty = vertices[vi + 1]
const c = vertices[vi + 2]
const pi = vertices[vi + 4]
const px = vertices[pi ]
const py = vertices[pi + 1]
const pc = vertices[pi + 2]
ScenePathFinder.closeVertex(oi)
// 如果到达终点,擦除数据并建立路径
if (tx === dx && ty === dy) {
ScenePathFinder.clear()
return this.buildPath(startX, startY, destX, destY, vi, passage)
}
// 遍历8个偏移方向的顶点
for (let i = 0; i < 16; i += 2) {
const nx = tx + offsets[i]
const ny = ty + offsets[i + 1]
// 如果顶点在有效网格区域内
if (tx >= 0 && tx < width && ty >= 0 && ty < height) {
const cost = ScenePathFinder.calculateExtraCostBetween(tx, ty, nx, ny, passage)
if (cost === 0 && ScenePathFinder.isInLineOfSight(px, py, nx, ny, passage)) {
// 如果相邻网格可通行,且与父节点可见
// 则计算到父节点的成本和到终点的期望值,打开该顶点
const nc = pc + Math.dist(nx, ny, px, py)
const ne = nc + Math.dist(nx, ny, dx, dy) * H_WEIGHT
ScenePathFinder.openVertex(nx, ny, nc, ne, pi, false)
} else {
// 否则计算相邻成本,增加额外成本
// 以及计算到终点的期望值,打开该顶点
const nc = c + Math.dist(nx, ny, tx, ty) + cost
const ne = nc + Math.dist(nx, ny, dx, dy) * H_WEIGHT
ScenePathFinder.openVertex(nx, ny, nc, ne, vi, cost === 0)
}
}
}
}
}
ScenePathFinder.clear()
ScenePathFinder.terrains = null
ScenePathFinder.obstacles = null
// 没有找到路径,返回单位路径
return ScenePathFinder.createUnitPath(destX, destY)
}
/**
* 创建单位移动路径
* @param {number} destX 终点位置X
* @param {number} destY 终点位置Y
* @returns {Float64Array} 角色移动路径
*/
createUnitPath(destX, destY) {
const path = new Float64Array(2)
path[0] = destX
path[1] = destY
path.index = 0
return path
}
/**
* 使用寻路后的数据建立路径
* @param {number} startX 起点位置X
* @param {number} startY 起点位置Y
* @param {number} destX 终点位置X
* @param {number} destY 终点位置Y
* @param {number} endIndex 终点顶点索引
* @returns {Float64Array} 角色移动路径
*/
buildPath(startX, startY, destX, destY, endIndex, passage) {
const vertices = ScenePathFinder.vertices
const radius = ActorCollider.sceneCollisionRadius
const caches = GL.arrays[1].float64
let blocked = false
let vi = endIndex
let ci = caches.length
caches[--ci] = destY
caches[--ci] = destX
while (true) {
// 丢弃不可通行的节点
while (vertices[vi + 5]) {
blocked = true
vi = vertices[vi + 4]
}
// 获取父节点索引
vi = vertices[vi + 4]
// 如果到达起点,跳出
if (vertices[vi + 2] === 0) {
break
}
// 插入中转点到缓存
const x = vertices[vi]
const y = vertices[vi + 1]
caches[--ci] = y + 0.5
caches[--ci] = x + 0.5
}
// 插入起点到缓存
caches[--ci] = startY
caches[--ci] = startX
// 调整终点坐标(要求可通行)
if (!blocked) {
const width = ScenePathFinder.width
const height = ScenePathFinder.height
const terrains = ScenePathFinder.terrains
const i = caches.length - 2
const x = Math.floor(caches[i])
const y = Math.floor(caches[i + 1])
const bi = x + y * width
// 如果左边不可通行,限制水平位置避免撞墙
if (x > 0 && terrains[bi - 1] !== passage) {
caches[i] = Math.max(caches[i], x + radius)
}
// 如果右边不可通行,限制水平位置避免撞墙
if (x < width - 1 && terrains[bi + 1] !== passage) {
caches[i] = Math.min(caches[i], x + 1 - radius)
}
// 如果上边不可通行,限制垂直位置避免撞墙
if (y > 0 && terrains[bi - width] !== passage) {
caches[i + 1] = Math.max(caches[i + 1], y + radius)
}
// 如果下边不可通行,限制垂直位置避免撞墙
if (y < height - 1 && terrains[bi + width] !== passage) {
caches[i + 1] = Math.min(caches[i + 1], y + 1 - radius)
}
}
// 调整最后一个拐点(如果存在)
// 已发现问题:角色可能会卡在这个拐点(靠近墙,无法到达目的地)
const pi = caches.length - 6
if (!blocked && pi >= 0) {
const px = caches[pi]
const py = caches[pi + 1]
const cx = caches[pi + 2]
const cy = caches[pi + 3]
const ex = Math.floor(caches[pi + 4])
const ey = Math.floor(caches[pi + 5])
const dist = Math.dist(cx, cy, px, py)
for (let i = 1; i < dist; i++) {
// 连接最后第2、3个点,计算插值节点
const ratio = i / dist
const x = cx * (1 - ratio) + px * ratio
const y = cy * (1 - ratio) + py * ratio
const dx = Math.floor(x)
const dy = Math.floor(y)
// 如果插值节点与终点可见,则设置最后第2个节点为该点
if (ScenePathFinder.isInLineOfSight(ex, ey, dx, dy)) {
caches[pi + 2] = x
caches[pi + 3] = y
} else {
break
}
}
}
// 调整中转点坐标(碰撞半径不为0.5)
if (radius !== 0.5) {
const end = caches.length - 2
for (let i = ci + 2; i < end; i += 2) {
const x0 = caches[i]
const y0 = caches[i + 1]
const x1 = caches[i - 2]
const y1 = caches[i - 1]
const x2 = caches[i + 2]
const y2 = caches[i + 3]
const radian1 = Math.modRadians(Math.atan2(y1 - y0, x1 - x0))
const radian2 = Math.modRadians(Math.atan2(y2 - y0, x2 - x0))
// 求中转点拐角平分线的弧度
const radian = Math.abs(radian1 - radian2) < Math.PI
? (radian1 + radian2) / 2
: (radian1 + radian2) / 2 + Math.PI
const horizontal = Math.cos(radian)
const vertical = Math.sin(radian)
const x = Math.floor(x0)
const y = Math.floor(y0)
// 假设中转点附近一定有墙的拐角,调整中转点,让它贴近墙面
// 根据拐角平分线的水平和垂直分量来判定4个方位的拐角
if (horizontal < -0.0001) caches[i] = x + radius
if (horizontal > 0.0001) caches[i] = x + 1 - radius
if (vertical < -0.0001) caches[i + 1] = y + radius
if (vertical > 0.0001) caches[i + 1] = y + 1 - radius
}
}
// 创建移动路径(不包括起点位置)
const path = caches.slice(ci + 2)
path.index = 0
ScenePathFinder.terrains = null
ScenePathFinder.obstacles = null
return path
}
/**
* 打开路径顶点
* @param {number} x 场景图块X
* @param {number} y 场景图块Y
* @param {number} cost 已知路径成本
* @param {number} expectation 路径总成本期望值
* @param {number} parentIndex 父级顶点的索引
* @param {number} blocked 与上一个顶点之间是否阻塞
*/
openVertex = (x, y, cost, expectation, parentIndex, blocked) => {
const si = x + y * 512
const vi = si * 6
switch (ScenePathFinder.states[si]) {
case 0:
// 如果顶点是关闭状态,则将其插入开启列表
for (let i = 0; i <= ScenePathFinder.opensetCount; i++) {
if (ScenePathFinder.openset[i] === 0) {
if (ScenePathFinder.opensetCount === i) {
ScenePathFinder.opensetCount++
}
// 设置顶点数据
ScenePathFinder.vertices[vi ] = x
ScenePathFinder.vertices[vi + 1] = y
ScenePathFinder.vertices[vi + 2] = cost
ScenePathFinder.vertices[vi + 3] = expectation
ScenePathFinder.vertices[vi + 4] = parentIndex
ScenePathFinder.vertices[vi + 5] = blocked
ScenePathFinder.openset[i] = vi + 1
// 设置为打开状态
ScenePathFinder.states[si] = 1
ScenePathFinder.indices[ScenePathFinder.stateCount++] = si
return
}
}
return
case 1:
// 如果顶点是打开状态,且新的数据成本更低,则替换
if (ScenePathFinder.vertices[vi + 2] > cost) {
ScenePathFinder.vertices[vi + 2] = cost
ScenePathFinder.vertices[vi + 3] = expectation
ScenePathFinder.vertices[vi + 4] = parentIndex
ScenePathFinder.vertices[vi + 5] = blocked
}
return
}
}
/**
* 关闭路径顶点
* @param {number} openedIndex 打开的顶点索引
*/
closeVertex = openedIndex => {
ScenePathFinder.openset[openedIndex] = 0
// 如果顶点正好处于尾部,则减少开启列表的计数
if (ScenePathFinder.opensetCount === openedIndex + 1) {
ScenePathFinder.opensetCount = openedIndex
}
}
/**
* 更新顶点队列
* @returns {boolean} 是否还有可用的顶点
*/
updateQueue = () => {
let count = 0
let expectation = Infinity
// 遍历开启列表中的顶点
const {vertices, openset, queue, opensetCount} = ScenePathFinder
for (let oi = 0; oi < opensetCount; oi++) {
// openset[oi] = 0为空
// openset[oi] > 0为有效顶点
const vi = openset[oi] - 1
if (vi >= 0) {
const ve = vertices[vi + 3]
if (ve > expectation) {
// 如果顶点期望值超出,跳过
continue
}
if (ve < expectation) {
// 如果顶点期望值较低,重置队列
// 且把该顶点作为队列中第一个数据
expectation = ve
queue[0] = oi
count = 1
} else if (count < 100) {
// 如果顶点期望值持平,添加顶点到队列中
queue[count++] = oi
}
}
}
if (expectation < ScenePathFinder.threshold) {
// 如果最终期望值小于阈值,则设置队列长度,返回true(继续)
ScenePathFinder.queueCount = count
return true
} else {
// 否则,则重置队列长度,返回false(中断)
ScenePathFinder.queueCount = 0
return false
}
}
/** 擦除寻路数据 */
clear = () => {
// 擦除顶点的状态数据
const {states, indices, openset} = ScenePathFinder
const {stateCount, opensetCount} = ScenePathFinder
for (let i = 0; i < stateCount; i++) {
states[indices[i]] = 0
}
ScenePathFinder.stateCount = 0
// 擦除已打开的顶点数据(擦除首个数据即可)
for (let i = 0; i < opensetCount; i++) {
openset[i] = 0
}
ScenePathFinder.opensetCount = 0
}
/**
* 设置绕开角色开关
* @param {boolean} bypass
*/
setBypass(bypass) {
switch (bypass) {
case false:
this.calculateExtraCostBetween = ScenePathFinder.normalCalculateExtraCostBetween
this.isInLineOfSight = ScenePathFinder.normalIsInLineOfSight
break
case true:
this.calculateExtraCostBetween = ScenePathFinder.bypassCalculateExtraCostBetween
this.isInLineOfSight = ScenePathFinder.bypassIsInLineOfSight
break
}
}
/**
* 计算相邻图块之间的额外通行成本(默认起点图块是可通行的)
* @param {number} sx 起点图块X
* @param {number} sy 起点图块Y
* @param {number} dx 终点图块X
* @param {number} dy 终点图块Y
* @returns {boolean}
*/
normalCalculateExtraCostBetween = (sx, sy, dx, dy, passage) => {
const dIndex = dx + dy * ScenePathFinder.width
if (sx === dx || sy === dy) {
if (ScenePathFinder.terrains[dIndex] !== passage) {
return ScenePathFinder.terrainObstacleCost
}
return 0
}
const vIndex = sx + dy * ScenePathFinder.width
const hIndex = dx + sy * ScenePathFinder.width
if (ScenePathFinder.terrains[dIndex] !== passage ||
ScenePathFinder.terrains[vIndex] !== passage ||
ScenePathFinder.terrains[hIndex] !== passage) {
return ScenePathFinder.terrainObstacleCost
}
return 0
}
/**
* 计算相邻图块之间的额外通行成本(绕开角色)
* @param {number} sx 起点图块X
* @param {number} sy 起点图块Y
* @param {number} dx 终点图块X
* @param {number} dy 终点图块Y
* @returns {boolean}
*/
bypassCalculateExtraCostBetween = (sx, sy, dx, dy, passage) => {
const dIndex = dx + dy * ScenePathFinder.width
if (sx === dx || sy === dy) {
if (ScenePathFinder.terrains[dIndex] !== passage) {
return ScenePathFinder.terrainObstacleCost
}
if (ScenePathFinder.obstacles[dIndex] !== 0) {
return ScenePathFinder.actorObstacleCost
}
return 0
}
const vIndex = sx + dy * ScenePathFinder.width
const hIndex = dx + sy * ScenePathFinder.width
if (ScenePathFinder.terrains[dIndex] !== passage ||
ScenePathFinder.terrains[vIndex] !== passage ||
ScenePathFinder.terrains[hIndex] !== passage) {
return ScenePathFinder.terrainObstacleCost
}
if (ScenePathFinder.obstacles[dIndex] !== 0 ||
ScenePathFinder.obstacles[vIndex] !== 0 ||
ScenePathFinder.obstacles[hIndex] !== 0) {
return ScenePathFinder.actorObstacleCost
}
return 0
}
/**
* 判断目标点是否在视线内
* @param {number} sx 起始图块X
* @param {number} sy 起始图块Y
* @param {number} dx 终点图块X
* @param {number} dy 终点图块Y
* @returns {boolean}
*/
normalIsInLineOfSight = (sx, sy, dx, dy, passage) => {
// 如果两点的曼哈顿距离大于80,直接返回false(不可视)
if (Math.abs(sx - dx) + Math.abs(sy - dy) > 80) {
return false
}
const width = ScenePathFinder.width
const terrains = ScenePathFinder.terrains
if (sx !== dx) {
// 如果水平坐标不同
const unitY = (dy - sy) / (dx - sx)
const step = sx < dx ? 1 : -1
const start = sx + step
const end = dx + step
const base = sy - (sx + step / 2) * unitY
// 在水平方向上栅格化相交的网格区域
for (let x = start; x !== end; x += step) {
const y = base + x * unitY
const a = x + Math.floor(y) * width
const b = x + Math.ceil(y) * width
// 连接起点和终点,连线被垂直网格线切分成若干点
// 如果其中一个交点上下偏移0.5距离的网格区域不能通行,则不可视
if (terrains[a] !== passage || terrains[b] !== passage) {
return false
}
}
}
if (sy !== dy) {
// 如果垂直坐标不同
const unitX = (dx - sx) / (dy - sy)
const step = sy < dy ? 1 : -1
const start = sy + step
const end = dy + step
const base = sx - (sy + step / 2) * unitX
// 在垂直方向上栅格化相交的网格区域
for (let y = start; y !== end; y += step) {
const x = base + y * unitX
const a = Math.floor(x) + y * width
const b = Math.ceil(x) + y * width
// 连接起点和终点,连线被水平网格线切分成若干点
// 如果其中一个交点左右偏移0.5距离的网格区域不能通行,则不可视
if (terrains[a] !== passage || terrains[b] !== passage) {
return false
}
}
}
// 两点可视
return true
}
/**
* 判断目标点是否在视线内(绕开角色)
* @param {number} sx 起始图块X
* @param {number} sy 起始图块Y
* @param {number} dx 终点图块X
* @param {number} dy 终点图块Y
* @returns {boolean}
*/
bypassIsInLineOfSight = (sx, sy, dx, dy, passage) => {
// 如果两点的曼哈顿距离大于80,直接返回false(不可视)
if (Math.abs(sx - dx) + Math.abs(sy - dy) > 80) {
return false
}
const width = ScenePathFinder.width
const terrains = ScenePathFinder.terrains
const obstacles = ScenePathFinder.obstacles
if (sx !== dx) {
// 如果水平坐标不同
const unitY = (dy - sy) / (dx - sx)
const step = sx < dx ? 1 : -1
const start = sx + step
const end = dx + step
const base = sy - (sx + step / 2) * unitY
// 在水平方向上栅格化相交的网格区域
for (let x = start; x !== end; x += step) {
const y = base + x * unitY
const a = x + Math.floor(y) * width
const b = x + Math.ceil(y) * width
// 连接起点和终点,连线被垂直网格线切分成若干点
// 如果其中一个交点上下偏移0.5距离的网格区域不能通行,则不可视
if (terrains[a] !== passage ||
terrains[b] !== passage ||
obstacles[a] !== 0 ||
obstacles[b] !== 0) {
return false
}
}
}
if (sy !== dy) {
// 如果垂直坐标不同
const unitX = (dx - sx) / (dy - sy)
const step = sy < dy ? 1 : -1
const start = sy + step
const end = dy + step
const base = sx - (sy + step / 2) * unitX
// 在垂直方向上栅格化相交的网格区域
for (let y = start; y !== end; y += step) {
const x = base + y * unitX
const a = Math.floor(x) + y * width
const b = Math.ceil(x) + y * width
// 连接起点和终点,连线被水平网格线切分成若干点
// 如果其中一个交点左右偏移0.5距离的网格区域不能通行,则不可视
if (terrains[a] !== passage ||
terrains[b] !== passage ||
obstacles[a] !== 0 ||
obstacles[b] !== 0) {
return false
}
}
}
// 两点可视
return true
}
}
// ******************************** 场景上下文类 ********************************
class SceneContext {
/** 场景文件ID
* @type {string}
*/ id
/** 场景数据
* @type {Object}
*/ data
/** 启用状态
* @type {boolean}
*/ enabled
/** 子场景列表
* @type {Array<Object>}
*/ subscenes
/** 场景宽度(0-512)
* @type {number}
*/ width
/** 场景高度(0-512)
* @type {number}
*/ height
/** 场景图块宽度(16-256)
* @type {number}
*/ tileWidth
/** 场景图块高度(16-256)
* @type {number}
*/ tileHeight
/** 场景地形数据(地面:0, 水面:1, 墙块: 2)
* @type {SceneTerrainArray}
*/ terrains
/** 场景地形障碍数据(地面:0, 水面:1, 墙块: 2)
* @type {Uint8Array}
*/ terrainObstacles
/** 场景障碍数据
* @type {SceneObstacleArray}
*/ obstacles
/** 场景图块动画已播放时间
* @type {number}
*/ elapsed
/** 场景图块动画播放间隔
* @type {number}
*/ animInterval
/** 场景图块动画帧计数
* @type {number}
*/ animFrame
/** 最大的角色碰撞器半径
* @type {number}
*/ maxColliderHalf
/** 最大的网格分区大小
* @type {number}
*/ maxGridCellSize
/** 场景事件映射表
* @type {Object}
*/ events
/** 场景脚本管理器
* @type {Script}
*/ script
/** 场景视差图管理器
* @type {SceneParallaxManager}
*/ parallaxes
/** 场景角色管理器
* @type {SceneActorList}
*/ actors
/** 场景动画管理器
* @type {SceneAnimationList}
*/ animations
/** 场景触发器管理器
* @type {SceneTriggerList}
*/ triggers
/** 场景区域管理器
* @type {SceneRegionList}
*/ regions
/** 场景光源管理器
* @type {SceneLightManager}
*/ lights
/** 场景粒子发射器管理器
* @type {SceneParticleEmitterList}
*/ emitters
/** 场景对象数据列表
* @type {Array<Object>}
*/ objects
/** 场景更新器模块列表
* @type {ModuleList}
*/ updaters
/** 场景渲染器模块列表
* @type {ModuleList}
*/ renderers
/** ID->场景对象映射表
* @type {Object}
*/ idMap
/** 场景加载数据Promise对象
* @type {Promise<SceneContext>}
*/ promise
/** 场景转换图块坐标到像素坐标方法
* @type {Function}
*/ convert
/** 场景转换图块坐标到像素坐标方法(浮点参数版本)
* @type {Function}
*/ convert2f
/**
* 场景上下文对象
* @param {string} id 场景文件ID
*/
constructor(id) {
this.id = id
this.enabled = false
// 禁用部分场景方法
this.update = Function.empty
this.render = Function.empty
this.emit = Function.empty
// 创建场景组件
this.idMap = new SceneObjectMap()
this.parallaxes = new SceneParallaxManager(this)
this.actors = new SceneActorList(this)
this.animations = new SceneAnimationList(this)
this.triggers = new SceneTriggerList(this)
this.regions = new SceneRegionList(this)
this.lights = new SceneLightManager(this)
this.emitters = new SceneParticleEmitterList(this)
// 设置更新器
this.updaters = new ModuleList(
this.parallaxes,
this.actors,
this.animations,
this.triggers,
this.regions,
this.lights,
Camera,
this.emitters,
)
// 设置渲染器
this.renderers = new ModuleList(
this.lights,
this.parallaxes.backgrounds,
Scene.spriteRenderer,
Scene.directLightRenderer,
this.parallaxes.foregrounds,
)
// 发送场景创建事件
Scene.emit('create', this)
}
/**
* 更新场景模块
* @param {number} deltaTime 时间增量(毫秒)
*/
update(deltaTime) {
this.elapsed += deltaTime
if (this.elapsed > this.animInterval) {
this.elapsed -= this.animInterval
// 更新图块动画帧
this.animFrame += 1
}
this.updaters.update(deltaTime)
}
/** 渲染场景画面 */
render() {
this.renderers.render()
}
/** 加载场景数据 */
async load() {
let resolve
this.promise = new Promise(r => {resolve = r})
const sceneIds = [this.id, ...(this.savedData?.subscenes ?? [])]
const promises = sceneIds.map(id => Data.loadScene(id))
const scenes = await Promise.all(promises)
for (let i = 0; i < scenes.length; i++) {
scenes[i].id = sceneIds[i]
scenes[i].path = File.getPathByGUID(sceneIds[i])
}
const scene = scenes[0]
const width = this.savedData?.width ?? scene.width
const height = this.savedData?.height ?? scene.height
const ambient = this.savedData?.ambient ?? scene.ambient
const terrains = this.savedData?.terrains ?? scene.terrains
this.data = scene
this.width = width
this.height = height
this.tileWidth = scene.tileWidth
this.tileHeight = scene.tileHeight
this.ambient = ambient
this.terrains = Codec.decodeTerrains(this, terrains, width, height)
this.terrainObstacles = new Uint8Array(this.terrains)
this.obstacles = new SceneObstacleArray(width, height)
this.animInterval = Data.config.scene.animationInterval
this.elapsed = 0
this.animFrame = 0
this.maxColliderHalf = 0
this.maxGridCellSize = 0
this.subscenes = scenes.slice(1)
this.objects = scene.objects
this.actors.cells.optimize(this)
this.actors.cells.resize(this)
// 恢复被禁用的场景方法
delete this.update
delete this.render
delete this.emit
SceneContext.createClosureMethods(this)
resolve(this)
return this
}
/**
* 获取场景指定区域中的角色
* @param {Object} $
* @param {number} $.x 选择区域中心X
* @param {number} $.y 选择区域中心Y
* @param {string} [$.area] 选择区域
* @param {number} [$.size] 正方形区域边长
* @param {number} [$.radius] 圆形区域半径
* @param {string} [$.selector] 选择器
* @param {string} [$.teamId] 选择器队伍ID
* @param {string} [$.condition] 条件
* @param {string} [$.attribute] 属性键
* @param {string} [$.divisor] 除数属性键
* @param {string} [$.activation] 激活状态条件
* @param {Actor|null} [$.exclusionActor] 排除角色
* @param {string} [$.exclusionTeamId] 排除队伍ID
* @returns {Actor|undefined} 目标角色
*/
getActor({
x,
y,
area = 'square',
size = 1,
radius = 0.5,
selector = 'any',
teamId = '',
condition = 'nearest',
attribute = '',
divisor = '',
activation = 'active',
exclusionActor = null,
exclusionTeamId = '',
}) {
const inspector = SceneContext.actorInspectors[selector]
const teamIndex = Team.get(teamId)?.index ?? -1
let count = 0
let skipCond
switch (activation) {
case 'active':
skipCond = false
break
case 'inactive':
skipCond = true
case 'either':
break
}
switch (area) {
case 'square': {
const half = size / 2
const cells = Scene.actors.cells.get(x - half, y - half, x + half, y + half)
const length = cells.count
for (let i = 0; i < length; i++) {
for (const actor of cells[i]) {
if (actor.active === skipCond ||
actor === exclusionActor ||
actor.teamId === exclusionTeamId) {
continue
}
const distX = Math.abs(x - actor.x)
const distY = Math.abs(y - actor.y)
if (distX <= half && distY <= half && inspector(actor.teamIndex, teamIndex)) {
CacheList[count++] = actor
}
}
}
break
}
case 'circle': {
const cells = this.actors.cells.get(x - radius, y - radius, x + radius, y + radius)
const length = cells.count
for (let i = 0; i < length; i++) {
for (const actor of cells[i]) {
if (actor.active === skipCond ||
actor === exclusionActor ||
actor.teamId === exclusionTeamId) {
continue
}
const dist = (x - actor.x) ** 2 + (y - actor.y) ** 2
if (dist <= radius ** 2 && inspector(actor.teamIndex, teamIndex)) {
CacheList[count++] = actor
}
}
}
break
}
}
CacheList.count = count
return SceneContext.actorFilters[condition](x, y, attribute, divisor)
}
/**
* 获取场景指定区域中的多个角色
* @param {Object} $
* @param {number} $.x 选择区域中心X
* @param {number} $.y 选择区域中心Y
* @param {string} [$.area] 选择区域
* @param {number} [$.width] 矩形区域宽度
* @param {number} [$.height] 矩形区域高度
* @param {number} [$.radius] 圆形区域半径
* @param {string} [$.selector] 选择器
* @param {string} [$.teamId] 选择器队伍ID
* @param {string} [$.activation] 激活状态条件
* @returns {Actor|undefined} 目标角色
*/
getMultipleActors({
x,
y,
area = 'rectangle',
width = 1,
height = 1,
radius = 0.5,
selector = 'any',
teamId = '',
activation = 'active',
}) {
const inspector = SceneContext.actorInspectors[selector]
const teamIndex = Team.get(teamId)?.index ?? -1
const actors = []
let skipCond
switch (activation) {
case 'active':
skipCond = false
break
case 'inactive':
skipCond = true
case 'either':
break
}
switch (area) {
case 'rectangle': {
const halfW = width / 2
const halfH = height / 2
const cells = Scene.actors.cells.get(x - halfW, y - halfH, x + halfW, y + halfH)
const length = cells.count
for (let i = 0; i < length; i++) {
for (const actor of cells[i]) {
if (actor.active === skipCond) {
continue
}
const distX = Math.abs(x - actor.x)
const distY = Math.abs(y - actor.y)
if (distX <= halfW && distY <= halfH && inspector(actor.teamIndex, teamIndex)) {
actors.push(actor)
}
}
}
break
}
case 'circle': {
const cells = this.actors.cells.get(x - radius, y - radius, x + radius, y + radius)
const length = cells.count
for (let i = 0; i < length; i++) {
for (const actor of cells[i]) {
if (actor.active === skipCond) {
continue
}
const dist = (x - actor.x) ** 2 + (y - actor.y) ** 2
if (dist <= radius ** 2 && inspector(actor.teamIndex, teamIndex)) {
actors.push(actor)
}
}
}
break
}
}
return actors
}
/**
* 设置场景环境光
* @param {number} red 红[0-255]
* @param {number} green 绿[0-255]
* @param {number} blue 蓝[0-255]
* @param {number} [easingId] 过渡曲线ID
* @param {number} [duration] 持续时间(毫秒)
*/
setAmbientLight(red, green, blue, easingId, duration) {
const updaters = this.updaters
const ambient = this.ambient
if (duration > 0) {
let elapsed = 0
const sRed = ambient.red
const sGreen = ambient.green
const sBlue = ambient.blue
const easing = Easing.get(easingId)
// 创建ambient更新器
updaters.set('ambient', {
update: deltaTime => {
elapsed += deltaTime
const time = easing.map(elapsed / duration)
ambient.red = Math.clamp(sRed * (1 - time) + red * time, 0, 255)
ambient.green = Math.clamp(sGreen * (1 - time) + green * time, 0, 255)
ambient.blue = Math.clamp(sBlue * (1 - time) + blue * time, 0, 255)
GL.setAmbientLight(ambient)
// 过渡结束,延迟删除更新器
if (elapsed >= duration) {
updaters.deleteDelay('ambient')
}
}
})
} else {
updaters.deleteDelay('ambient')
ambient.red = red
ambient.green = green
ambient.blue = blue
GL.setAmbientLight(ambient)
}
}
/** 加载地形障碍 */
loadTerrainObstacles() {
const {width, height} = this
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
this.updateTerrainObstacle(x, y)
}
}
}
/**
* 更新地形障碍
* @param {number} x 场景X
* @param {number} y 场景Y
*/
updateTerrainObstacle(x, y) {
const ti = x + y * this.width
this.terrainObstacles[ti] =
this.terrains[ti] ||
this.parallaxes.getTileTerrain(x, y)
}
/**
* 加载子场景
* @param {string} sceneId 场景ID
* @param {number} [offsetX] 对象偏移X
* @param {number} [offsetY] 对象偏移Y
*/
async loadSubscene(sceneId, offsetX = 0, offsetY = 0) {
const scene = await Data.loadScene(sceneId)
scene.id = sceneId
scene.path = File.getPathByGUID(sceneId)
await this.promise
// 偏移场景对象
if (offsetX !== 0 || offsetY !== 0) {
const shift = nodes => {
for (const node of nodes) {
switch (node.class) {
case 'folder':
shift(node.children)
continue
default:
node.x += offsetX
node.y += offsetY
continue
}
}
}
shift(scene.objects)
}
// 如果不存在相同的子场景,添加到子场景列表
if (!this.subscenes.find(scene => scene.id === sceneId)) {
this.subscenes.push(scene)
}
// 创建场景对象并发送自动执行事件
const instances = this.loadObjects(scene.objects, scene.path)
for (const instance of instances) {
instance.autorun()
}
}
/**
* 卸载子场景
* @param {string} sceneId 场景ID
*/
unloadSubscene(sceneId) {
const scene = this.subscenes.find(
scene => scene.id === sceneId
)
if (!scene) return
this.subscenes.remove(scene)
const presetIdMap = {}
const register = nodes => {
for (const node of nodes) {
if (node.class === 'folder') {
register(node.children)
} else {
presetIdMap[node.presetId] = true
}
}
}
register(scene.objects)
// 获取待删除对象
// 避免写在回调函数中异步执行
const deletedObjects = []
for (const manager of [this.parallaxes, this.lights]) {
for (const group of manager.groups) {
let i = group.length
while (--i >= 0) {
const instance = group[i]
if (presetIdMap[instance.presetId]) {
deletedObjects.push({instance, manager})
}
}
}
}
for (const manager of [this.actors, this.animations, this.regions, this.emitters]) {
let i = manager.length
while (--i >= 0) {
const instance = manager[i]
if (presetIdMap[instance.presetId]) {
deletedObjects.push({instance, manager})
}
}
}
Callback.push(() => {
for (const {instance, manager} of deletedObjects) {
instance.destroy()
manager.remove(instance)
}
})
}
/**
* 加载初始场景对象
* @param {Array} objectList 场景对象数据列表
* @param {string} scenePath 场景路径
* @returns {Array} 已创建的场景对象实例列表
*/
loadObjects(objectList, scenePath) {
const instances = []
const isNew = !this.savedData
const compile = Data.compileEvents
const test = Scene.testConditions
const actors = this.actors
const animations = this.animations
const regions = this.regions
const lights = this.lights
const parallaxes = this.parallaxes
const emitters = this.emitters
const loaders = {
folder: node => load(node.children),
actor: node => {
const actorId = node.actorId
let data = Data.actors[actorId]
if (data !== undefined) {
const {events, scripts} = node
if (events.length + scripts.length !== 0) {
// 修改角色数据,添加场景预设的事件和脚本
data = Object.create(data)
data.events = {
...data.events,
...node.events,
}
data.scripts = [
...data.scripts,
...node.scripts,
]
}
node.data = data
}
if (isNew && test(node)) {
const actor = new Actor(data)
actor.name = node.name
actor.presetId = node.presetId
actor.selfVarId = node.presetId
actor.setTeam(node.teamId)
actor.setPosition(node.x, node.y)
actor.updateAngle(Math.radians(node.angle))
if (node.scale !== 1) {
actor.setScale(node.scale * data.scale)
}
actors.append(actor)
instances.push(actor)
}
actors.presets[node.presetId] = node
},
animation: node => {
node.motion = Enum.getValue(node.motion)
if (isNew && test(node)) {
const data = Data.animations[node.animationId]
if (data !== undefined) {
const animation = new SceneAnimation(node, data)
animation.selfVarId = node.presetId
animation.setMotion(node.motion)
animation.setAngle(Math.radians(node.angle))
animations.append(animation)
instances.push(animation)
}
}
animations.presets[node.presetId] = node
},
particle: node => {
if (isNew && test(node)) {
const data = Data.particles[node.particleId]
if (data !== undefined) {
const emitter = new SceneParticleEmitter(node, data)
emitter.selfVarId = node.presetId
emitters.append(emitter)
instances.push(emitter)
}
}
emitters.presets[node.presetId] = node
},
region: node => {
if (isNew && test(node)) {
const region = new SceneRegion(node)
region.selfVarId = node.presetId
regions.append(region)
instances.push(region)
}
regions.presets[node.presetId] = node
},
light: node => {
if (isNew && test(node)) {
const light = new SceneLight(node)
light.selfVarId = node.presetId
lights.append(light)
instances.push(light)
}
lights.presets[node.presetId] = node
},
parallax: node => {
if (isNew && test(node)) {
const parallax = new SceneParallax(node)
parallax.selfVarId = node.presetId
parallaxes.append(parallax)
instances.push(parallax)
}
parallaxes.presets[node.presetId] = node
},
tilemap: node => {
if (isNew && test(node)) {
const tilemap = new SceneTilemap(this, node)
tilemap.selfVarId = node.presetId
parallaxes.append(tilemap)
instances.push(tilemap)
}
parallaxes.presets[node.presetId] = node
},
}
const load = nodes => {
const length = nodes.length
for (let i = 0; i < length; i++) {
const node = nodes[i]
if (node.events) {
compile(node, `${scenePath}\n@ ${node.name}.${node.presetId}`)
}
loaders[node.class](node)
}
}
// 加载场景对象
load(objectList)
return instances
}
/**
* 调用场景事件
* @param {string} type 场景事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
return EventHandler.call(new EventHandler(commands), this.updaters)
}
}
/**
* 调用场景事件和脚本
* @param {string} type 场景事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
/** 销毁场景上下文 */
destroy() {
this.parallaxes.destroy()
this.actors.destroy()
this.animations.destroy()
this.triggers.destroy()
this.regions.destroy()
this.lights.destroy()
this.emitters.destroy()
this.emit('destroy')
// 发送场景销毁事件
Scene.emit('destroy', this)
// 删除不再引用的图像纹理
const manager = GL.textureManager
manager.updated = false
Callback.push(() => {
if (!manager.updated) {
manager.updated = true
manager.update()
}
})
}
/** 保存场景数据 */
saveData() {
return {
id: this.id,
subscenes: this.subscenes.map(scene => scene.id),
index: Scene.contexts.indexOf(this),
width: this.width,
height: this.height,
ambient: this.ambient,
terrains: this.terrains.saveData(),
actors: this.actors.saveData(),
animations: this.animations.saveData(),
emitters: this.emitters.saveData(),
regions: this.regions.saveData(),
lights: this.lights.saveData(),
parallaxes: this.parallaxes.saveData(),
}
}
/**
* 加载场景数据
* @param {Object} scene
*/
loadData(scene) {
this.savedData = scene
this.load()
}
// 默认方法
initialize() {}
convert() {return Scene.sharedPoint}
convert2f() {return Scene.sharedPoint}
/**
* 判断目标点是否在墙块中
* @param {number} x 场景坐标X
* @param {number} y 场景坐标Y
* @returns {boolean}
*/
isInWallBlock(x, y) {
return x >= 0 && y >= 0 && x < this.width && y < this.height &&
this.terrainObstacles[Math.floor(x) + Math.floor(y) * this.width] === 0b10
}
/**
* 判断起点和终点是否在视线内可见
* @param {number} sx 起点场景X
* @param {number} sy 起点场景Y
* @param {number} dx 终点场景X
* @param {number} dy 终点场景Y
* @returns {boolean}
*/
isInLineOfSight(sx, sy, dx, dy) {
const width = this.width
const height = this.height
// 如果坐标点在场景网格外,返回false(不可视)
if (sx < 0 || sx >= width ||
sy < 0 || sy >= height ||
dx < 0 || dx >= width ||
dy < 0 || dy >= height) {
return false
}
const {terrainObstacles} = this
const tsx = Math.floor(sx)
const tsy = Math.floor(sy)
const tdx = Math.floor(dx)
const tdy = Math.floor(dy)
if (tsx !== tdx) {
// 如果水平网格坐标不同
const unitY = (dy - sy) / (dx - sx)
const step = sx < dx ? 1 : -1
const start = tsx + step
const end = tdx + step
// 在水平方向上栅格化相交的地形
for (let x = start; x !== end; x += step) {
const _x = step > 0 ? x : x + 1
const y = Math.floor(sy + (_x - sx) * unitY)
// 连接起点和终点,连线被垂直网格线切分成若干点
// 如果其中一个交点的网格区域是墙块,则不可视
if (terrainObstacles[x + y * width] === 0b10) {
return false
}
}
}
if (tsy !== tdy) {
// 如果垂直网格坐标不同
const unitX = (dx - sx) / (dy - sy)
const step = sy < dy ? 1 : -1
const start = tsy + step
const end = tdy + step
// 在垂直方向上栅格化相交的地形
for (let y = start; y !== end; y += step) {
const _y = step > 0 ? y : y + 1
const x = Math.floor(sx + (_y - sy) * unitX)
// 连接起点和终点,连线被水平网格线切分成若干点
// 如果其中一个交点的网格区域是墙块,则不可视
if (terrainObstacles[x + y * width] === 0b10) {
return false
}
}
}
// 如果起点和终点的网格区域都不是墙块,则可视
return terrainObstacles[tsx + tsy * width] !== 0b10 &&
terrainObstacles[tdx + tdy * width] !== 0b10
}
/**
* 获取两点之间射线的第一个墙块位置
* @param {number} sx 起点场景X
* @param {number} sy 起点场景Y
* @param {number} dx 终点场景X
* @param {number} dy 终点场景Y
* @returns {Object|null}
*/
getWallPosByRay(sx, sy, dx, dy) {
const width = this.width
const height = this.height
let target = null
let weight = Infinity
// 如果坐标点在场景网格外
if (sx < 0 || sx >= width ||
sy < 0 || sy >= height ||
dx < 0 || dx >= width ||
dy < 0 || dy >= height) {
return target
}
const {terrainObstacles} = this
const tsx = Math.floor(sx)
const tsy = Math.floor(sy)
const tdx = Math.floor(dx)
const tdy = Math.floor(dy)
if (tsx !== tdx) {
// 如果水平网格坐标不同
const unitY = (dy - sy) / (dx - sx)
const step = sx < dx ? 1 : -1
const start = tsx + step
const end = tdx + step
// 在水平方向上栅格化相交的地形
for (let x = start; x !== end; x += step) {
const _x = step > 0 ? x : x + 1
const y = Math.floor(sy + (_x - sx) * unitY)
// 连接起点和终点,连线被垂直网格线切分成若干点
// 如果其中一个交点的网格区域是墙块
if (terrainObstacles[x + y * width] === 0b10) {
weight = Math.dist(sx, sy, x + 0.5, y + 0.5)
target = {x, y}
break
}
}
}
if (tsy !== tdy) {
// 如果垂直网格坐标不同
const unitX = (dx - sx) / (dy - sy)
const step = sy < dy ? 1 : -1
const start = tsy + step
const end = tdy + step
// 在垂直方向上栅格化相交的地形
for (let y = start; y !== end; y += step) {
const _y = step > 0 ? y : y + 1
const x = Math.floor(sx + (_y - sy) * unitX)
// 连接起点和终点,连线被水平网格线切分成若干点
// 如果其中一个交点的网格区域是墙块
if (terrainObstacles[x + y * width] === 0b10) {
const dist = Math.dist(sx, sy, x + 0.5, y + 0.5)
if (dist < weight) {
weight = dist
target = {x, y}
}
break
}
}
}
// 如果起点的网格区域是墙块
if (target === null && terrainObstacles[tsx + tsy * width] === 0b10) {
target = {x: tsx, y: tsy}
}
// 如果终点的网格区域是墙块
if (target === null && terrainObstacles[tdx + tdy * width] === 0b10) {
target = {x: tdx, y: tdy}
}
return target
}
/**
* 创建闭包方法
* @param {SceneContext} scene 场景上下文对象
*/
static createClosureMethods = scene => {
/** 场景初始化 */
scene.initialize = function () {
if (Scene.binding === this) {
Script.deferredLoading = true
this.events = Data.compileEvents(this.data, scene.data.path)
this.script = Script.create(this, this.data.scripts)
// 加载场景对象
this.loadObjects(this.objects, this.data.path)
// 加载子场景对象
for (const subscene of this.subscenes) {
this.loadObjects(subscene.objects, subscene.path)
}
// 加载存档数据
const data = this.savedData
if (data) {
this.actors.loadData(data.actors)
this.animations.loadData(data.animations)
this.emitters.loadData(data.emitters)
this.regions.loadData(data.regions)
this.lights.loadData(data.lights)
this.parallaxes.loadData(data.parallaxes)
// 立即更新回调函数
Callback.update()
} else {
this.emit('create')
}
Script.loadDeferredParameters()
// 发送自动执行事件
this.parallaxes.autorun()
this.actors.autorun()
this.animations.autorun()
this.regions.autorun()
this.lights.autorun()
this.emitters.autorun()
this.emit('autorun')
// 删除临时数据和初始化方法
delete this.data
delete this.savedData
delete this.initialize
// 发送场景初始化事件
if (!data) {
Scene.emit('initialize', this)
}
// 发送场景加载事件
Scene.emit('load', this)
}
}
const {floor} = Math
const point = Scene.sharedPoint
const {tileWidth, tileHeight} = scene
const {terrainObstacles, width, height} = scene
/**
* 转换场景坐标到像素坐标
* @param {{x: number, y: number}} tile 拥有场景坐标的对象
* @returns {{x: number, y: number}}
*/
scene.convert = tile => {
point.x = tile.x * tileWidth
point.y = tile.y * tileHeight
return point
}
/**
* 转换场景坐标到像素坐标(2个浮点参数版本)
* @param {number} x 场景坐标X
* @param {number} y 场景坐标Y
* @returns {{x: number, y: number}}
*/
scene.convert2f = (x, y) => {
point.x = x * tileWidth
point.y = y * tileHeight
return point
}
}
// 角色检查器集合
static actorInspectors = new class {
// 检查器 - 判断敌对角色
'enemy' = (teamIndex1, teamIndex2) => {
return Team.getRelationByIndexes(teamIndex1, teamIndex2) === 0
}
// 检查器 - 判断友好角色
'friend' = (teamIndex1, teamIndex2) => {
return Team.getRelationByIndexes(teamIndex1, teamIndex2) === 1
}
// 检查器 - 判断小队角色
'team' = (teamIndex1, teamIndex2) => {
return teamIndex1 === teamIndex2
}
// 检查器 - 判断任意角色
'any' = () => {
return true
}
}
// 角色过滤器集合
static actorFilters = new class {
// 角色过滤器 - 最近距离
'nearest' = (x, y) => this.compareDistance(x, y, false)
// 角色过滤器 - 最远距离
'farthest' = (x, y) => this.compareDistance(x, y, true)
// 角色过滤器 - 最小属性值
'min-attribute-value' = (x, y, attribute) => this.compareAttribute(attribute, false)
// 角色过滤器 - 最大属性值
'max-attribute-value' = (x, y, attribute) => this.compareAttribute(attribute, true)
// 角色过滤器 - 最小属性比率
'min-attribute-ratio' = (x, y, attribute, divisor) => this.compareAttributeRatio(attribute, divisor, false)
// 角色过滤器 - 最大属性比率
'max-attribute-ratio' = (x, y, attribute, divisor) => this.compareAttributeRatio(attribute, divisor, true)
// 角色过滤器 - 随机
'random' = () => {
return CacheList.count !== 0
? CacheList[Math.randomInt(0, CacheList.count - 1)]
: undefined
}
// 角色过滤器 - 比较距离大小
compareDistance = (x, y, greater) => {
let target
let weight = greater ? -Infinity : Infinity
const count = CacheList.count
for (let i = 0; i < count; i++) {
const actor = CacheList[i]
const distX = x - actor.x
const distY = y - actor.y
const dist = distX ** 2 + distY ** 2
if (dist > weight === greater) {
weight = dist
target = actor
}
}
return target
}
// 角色过滤器 - 比较属性值大小
compareAttribute = (key, greater) => {
let target
let weight = greater ? -Infinity : Infinity
const count = CacheList.count
for (let i = 0; i < count; i++) {
const actor = CacheList[i]
const value = actor.attributes[key]
if (value > weight === greater) {
target = actor
weight = value
}
}
return target
}
// 角色过滤器 - 比较属性比率大小
compareAttributeRatio = (key, divisor, greater) => {
let target
let weight = greater ? -Infinity : Infinity
const count = CacheList.count
for (let i = 0; i < count; i++) {
const actor = CacheList[i]
const attributes = actor.attributes
const ratio = attributes[key] / attributes[divisor]
if (ratio > weight === greater) {
target = actor
weight = ratio
}
}
return target
}
}
}
// ******************************** 场景对象映射表类 ********************************
class SceneObjectMap {
/**
* 设置对象到映射表
* @param {string} key 键
* @param {Object} object 场景对象
*/
set(key, object) {
if (key) {
this[key] = object
}
}
/**
* 从映射表中删除对象
* @param {string} key 键
* @param {Object} object 场景对象
*/
delete(key, object) {
if (key && this[key] === object) {
delete this[key]
}
}
}
// ******************************** 场景地形数组类 ********************************
class SceneTerrainArray extends Uint8Array {
/** 绑定的场景上下文对象
* @param {SceneContext}
*/ scene
/** 场景地形数组水平数量
* @type {number}
*/ width
/** 场景地形数组垂直数量
* @type {number}
*/ height
/**
* 场景地形数组对象
* @param {SceneContext|null} scene 场景上下文对象
* @param {number} width 宽度
* @param {number} height 高度
*/
constructor(scene, width, height) {
super(width * height)
this.scene = scene
this.width = width
this.height = height
}
// 获取地形数据
get(x, y) {
const {width, height} = this
if (x >= 0 && x < width && y >= 0 && y < height) {
return this[x + y * width]
}
return -1
}
// 设置地形数据
set(x, y, terrain) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
this[x + y * this.width] = terrain
this.scene.updateTerrainObstacle(x, y)
}
}
// 保存数据
saveData() {
return Codec.encodeTerrains(this)
}
}
// ******************************** 场景障碍数组类 ********************************
class SceneObstacleArray extends Uint32Array {
/** 场景障碍数组水平数量
* @type {number}
*/ width
/** 场景障碍数组垂直数量
* @type {number}
*/ height
/**
* 场景障碍数组对象
* @param {number} width 宽度
* @param {number} height 高度
*/
constructor(width, height) {
super(width * height)
this.width = width
this.height = height
}
// 获取障碍数据
get(x, y) {
const {width, height} = this
if (x < width && y < height) {
return this[x + y * width]
}
return -1
}
/**
* 更新场景对象的障碍(根据对象的位置来分配)
* @param {Actor} actor 角色对象
*/
update(actor) {
let gridId = -1
if (actor.collider.weight !== 0) {
const gridX = Math.floor(actor.x)
const gridY = Math.floor(actor.y)
if (this.isValidPosition(gridX, gridY)) {
gridId = gridX + gridY * this.width
}
}
if (actor.gridId !== gridId) {
// 从旧的网格中删除障碍
if (actor.gridId !== -1) {
this[actor.gridId]--
}
// 添加障碍到新的网格
if (gridId !== -1) {
this[gridId]++
}
actor.gridId = gridId
}
}
/**
* 添加场景对象障碍到数组中
* @param {Actor} actor 角色对象
*/
append(actor) {
if (actor.collider.weight !== 0) {
const gridX = Math.floor(actor.x)
const gridY = Math.floor(actor.y)
if (this.isValidPosition(gridX, gridY)) {
const gridId = gridX + gridY * this.width
this[gridId]++
actor.gridId = gridId
}
}
}
/**
* 从管理器中移除场景对象的障碍
* @param {Object} actor 拥有场景坐标的对象
*/
remove(actor) {
if (actor.gridId !== -1) {
this[actor.gridId]--
actor.gridId = -1
}
}
// 检查网格的位置是否有效
isValidPosition(gridX, gridY) {
return gridX >= 0 && gridX < this.width && gridY >= 0 && gridY < this.height
}
}
// ******************************** 场景视差图管理器类 ********************************
class SceneParallaxManager {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 视差图和瓦片地图的预设数据表
* @type {Object}
*/ presets
/** 背景层视差图群组
* @type {SceneParallaxGroup}
*/ backgrounds
/** 前景层视差图群组
* @type {SceneParallaxGroup}
*/ foregrounds
/** 对象层视差图群组
* @type {SceneParallaxGroup}
*/ doodads
/** 瓦片地图群组
* @type {SceneParallaxGroup}
*/ tilemaps
/** 视差图群组列表
* @type {Array<SceneParallaxGroup>}
*/ groups
/**
* 场景视差图管理器
* @param {SceneContext} scene 场景上下文对象
*/
constructor(scene) {
this.scene = scene
this.presets = {}
// 包含背景层、前景层、对象层对象列表
// 每个列表可加入视差图或瓦片地图对象(混合)
this.backgrounds = new SceneParallaxGroup()
this.foregrounds = new SceneParallaxGroup()
this.doodads = new SceneParallaxGroup()
this.tilemaps = new SceneParallaxGroup()
this.groups = [
this.backgrounds,
this.foregrounds,
this.doodads,
]
}
/**
* 获取指定位置的图块地形
* @param {number} x 场景X
* @param {number} y 场景Y
* @returns {number} 地形编码
*/
getTileTerrain(x, y) {
for (const tilemap of this.tilemaps) {
const terrain = tilemap.getTileTerrain(x, y)
if (terrain !== 0) return terrain
}
return 0
}
// 重新加载地形障碍
reloadTerrainObstacles() {
if (!this.reloadingTerrains) {
this.reloadingTerrains = true
Callback.push(() => {
delete this.reloadingTerrains
this.scene.loadTerrainObstacles()
})
}
}
/**
* 添加视差图到管理器中
* @param {SceneParallax|SceneTilemap} parallax 视差图或瓦片地图对象
*/
append(parallax) {
if (parallax.parent === null) {
parallax.parent = this
// 根据图层添加到对应的子列表中
switch (parallax.layer) {
case 'background':
this.backgrounds.push(parallax)
break
case 'foreground':
this.foregrounds.push(parallax)
break
case 'object':
this.doodads.push(parallax)
break
}
this.scene.idMap.set(parallax.presetId, parallax)
if (this.scene.enabled) {
parallax.autorun()
}
// 排序视差图层
if (!this.sorting) {
this.sorting = true
Callback.push(() => {
delete this.sorting
this.sort()
})
}
// 更新图块地形
if (parallax instanceof SceneTilemap) {
this.reloadTerrainObstacles()
}
}
}
/**
* 从管理器中移除视差图
* @param {SceneParallax|SceneTilemap} parallax 视差图或瓦片地图对象
*/
remove(parallax) {
if (parallax.parent === this) {
parallax.parent = null
for (const group of this.groups) {
if (group.remove(parallax)) {
break
}
}
if (parallax instanceof SceneTilemap) {
this.tilemaps.remove(parallax)
this.reloadTerrainObstacles()
}
this.scene.idMap.delete(parallax.presetId, parallax)
}
}
/**
* 更新管理器分组中的场景视差图
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
for (const group of this.groups) {
for (const parallax of group) {
parallax.update(deltaTime)
}
}
}
/** 排序视差图和瓦片地图图层 */
sort() {
for (const group of this.groups) {
group.sort()
}
this.tilemaps.length = 0
for (const group of this.groups) {
for (const parallax of group) {
if (parallax instanceof SceneTilemap) {
this.tilemaps.push(parallax)
}
}
}
// 颠倒瓦片地图顺序
// 优先读取上层地形数据
this.tilemaps.reverse()
}
/** 发送自动执行事件 */
autorun() {
for (const group of this.groups) {
for (const parallax of group) {
parallax.autorun()
}
}
}
/** 销毁管理器中的视差图和瓦片地图 */
destroy() {
for (const group of this.groups) {
for (const parallax of group) {
parallax.destroy()
}
}
}
/** 保存视差图数据 */
saveData() {
const data = []
for (const group of this.groups) {
for (const entity of group) {
if ('saveData' in entity) {
data.push(entity.saveData())
}
}
}
return data
}
/**
* 加载视差图数据
* @param {Object[]} parallaxes
*/
loadData(parallaxes) {
const presets = this.presets
const scene = Scene.binding
for (const savedData of parallaxes) {
const preset = presets[savedData.presetId]
if (preset) {
Object.setPrototypeOf(savedData, preset)
switch (preset.class) {
case 'parallax':
// 重新创建视差图实例
this.append(new SceneParallax(savedData))
continue
case 'tilemap':
// 重新创建瓦片地图实例
this.append(new SceneTilemap(scene, savedData))
continue
}
}
}
this.sort()
}
}
// ******************************** 场景视差图群组类 ********************************
class SceneParallaxGroup extends Array {
/** 渲染视差图或瓦片地图 */
render() {
for (const parallax of this) {
if (parallax.visible) {
parallax.draw()
}
}
}
/** 排序图层 */
sort() {
super.sort(SceneParallaxGroup.sorter)
}
/** 视差图层排序器函数 */
static sorter = (a, b) => a.order - b.order
}
// ******************************** 场景视差图类 ********************************
class SceneParallax {
/** 视差图可见性
* @type {boolean}
*/ visible
/** 视差图预设数据ID
* @type {string}
*/ presetId
/** 视差图独立变量ID
* @type {string}
*/ selfVarId
/** 视差图名称
* @type {string}
*/ name
/** 视差图图层
* @type {string}
*/ layer
/** 视差图排序优先级
* @type {number}
*/ order
/** 视差图光照采样模式
* @type {string}
*/ light
/** 视差图混合模式
* @type {string}
*/ blend
/** 视差图不透明度
* @type {number}
*/ opacity
/** 视差图水平位置
* @type {number}
*/ x
/** 视差图垂直位置
* @type {number}
*/ y
/** 视差图水平缩放系数
* @type {number}
*/ scaleX
/** 视差图垂直缩放系数
* @type {number}
*/ scaleY
/** 视差图水平重复次数
* @type {number}
*/ repeatX
/** 视差图垂直重复次数
* @type {number}
*/ repeatY
/** 视差图水平锚点
* @type {number}
*/ anchorX
/** 视差图垂直锚点
* @type {number}
*/ anchorY
/** 视差图水平偏移位置
* @type {number}
*/ offsetX
/** 视差图垂直偏移位置
* @type {number}
*/ offsetY
/** 水平视差系数
* @type {number}
*/ parallaxFactorX
/** 垂直视差系数
* @type {number}
*/ parallaxFactorY
/** 视差图水平移动速度
* @type {number}
*/ shiftSpeedX
/** 视差图垂直移动速度
* @type {number}
*/ shiftSpeedY
/** 视差图图像色调
* @type {Array<number>}
*/ tint
/** 视差图图像纹理
* @type {ImageTexture}
*/ texture
/** 视差图纹理水平偏移
* @type {number}
*/ shiftX
/** 视差图纹理垂直偏移
* @type {number}
*/ shiftY
/** 视差图更新器模块列表
* @type {ModuleList}
*/ updaters
/** 视差图事件映射表
* @type {Object}
*/ events
/** 视差图脚本管理器
* @type {Script}
*/ script
/** 视差图的父级对象
* @type {SceneParallaxManager|null}
*/ parent
/** 已开始状态
* @type {boolean}
*/ started
/**
* 场景视差图对象
* @param {Object} parallax 场景中预设的视差图数据
*/
constructor(parallax) {
this.visible = parallax.visible ?? true
this.presetId = parallax.presetId
this.selfVarId = parallax.selfVarId ?? ''
this.name = parallax.name
this.layer = parallax.layer
this.order = parallax.order
this.light = parallax.light
this.blend = parallax.blend
this.opacity = parallax.opacity
this.x = parallax.x
this.y = parallax.y
this.scaleX = parallax.scaleX
this.scaleY = parallax.scaleY
this.repeatX = parallax.repeatX
this.repeatY = parallax.repeatY
this.anchorX = parallax.anchorX
this.anchorY = parallax.anchorY
this.offsetX = parallax.offsetX
this.offsetY = parallax.offsetY
this.parallaxFactorX = parallax.parallaxFactorX
this.parallaxFactorY = parallax.parallaxFactorY
this.shiftSpeedX = parallax.shiftSpeedX
this.shiftSpeedY = parallax.shiftSpeedY
this.tint = {...parallax.tint}
this.texture = new ImageTexture(parallax.image, {sync: true})
this.shiftX = 0
this.shiftY = 0
this.updaters = new ModuleList()
this.events = parallax.events
this.script = Script.create(this, parallax.scripts)
this.parent = null
this.started = false
}
/**
* 更新场景视差图
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
this.updaters.update(deltaTime)
// 如果视差图的移动速度不为0,计算纹理滚动的位置
if (this.shiftSpeedX !== 0 || this.shiftSpeedY !== 0) {
const texture = this.texture
if (texture.complete) {
this.shiftX = (
this.shiftX
+ this.shiftSpeedX
* deltaTime / 1000
/ texture.width
) % 1
this.shiftY = (
this.shiftY
+ this.shiftSpeedY
* deltaTime / 1000
/ texture.height
) % 1
}
}
}
/** 绘制场景视差图 */
draw() {
const texture = this.texture
if (!texture.complete) {
return
}
const gl = GL
const parallax = this
const vertices = gl.arrays[0].float32
const pw = texture.width
* parallax.scaleX
* parallax.repeatX
const ph = texture.height
* parallax.scaleY
* parallax.repeatY
const ox = parallax.offsetX
const oy = parallax.offsetY
const ax = parallax.anchorX * pw
const ay = parallax.anchorY * ph
// 获取视差图锚点在场景中的像素位置,并计算出实际位置
const anchor = Scene.getParallaxAnchor(parallax)
const dl = anchor.x - ax + ox
const dt = anchor.y - ay + oy
const dr = dl + pw
const db = dt + ph
const cl = Camera.scrollLeft
const ct = Camera.scrollTop
const cr = Camera.scrollRight
const cb = Camera.scrollBottom
// 如果视差图在屏幕中可见,则绘制它
if (dl < cr && dr > cl && dt < cb && db > ct) {
const sl = this.shiftX
const st = this.shiftY
const sr = sl + parallax.repeatX
const sb = st + parallax.repeatY
vertices[0] = dl
vertices[1] = dt
vertices[2] = sl
vertices[3] = st
vertices[4] = dl
vertices[5] = db
vertices[6] = sl
vertices[7] = sb
vertices[8] = dr
vertices[9] = db
vertices[10] = sr
vertices[11] = sb
vertices[12] = dr
vertices[13] = dt
vertices[14] = sr
vertices[15] = st
gl.blend = parallax.blend
gl.alpha = parallax.opacity
const program = gl.imageProgram.use()
const tint = parallax.tint
const red = tint[0] / 255
const green = tint[1] / 255
const blue = tint[2] / 255
const gray = tint[3] / 255
const modeMap = SceneParallax.lightSamplingModes
const lightMode = parallax.light
const lightModeIndex = modeMap[lightMode]
const matrix = gl.matrix.project(
gl.flip,
cr - cl,
cb - ct,
).translate(-cl, -ct)
gl.bindVertexArray(program.vao.a110)
gl.vertexAttrib1f(program.a_Opacity, 1)
gl.uniformMatrix3fv(program.u_Matrix, false, matrix)
gl.uniform1i(program.u_LightMode, lightModeIndex)
if (lightMode === 'anchor') {
// 如果是光线采样模式为锚点采样,上传锚点位置
gl.uniform2f(program.u_LightCoord, anchor.x, anchor.y)
}
gl.uniform1i(program.u_ColorMode, 0)
gl.uniform4f(program.u_Tint, red, green, blue, gray)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW, 0, 16)
gl.bindTexture(gl.TEXTURE_2D, texture.base.glTexture)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
}
}
/**
* 调用视差图事件
* @param {string} type 视差图事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用视差图事件和脚本
* @param {string} type 视差图事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁视差图 */
destroy() {
if (this.texture) {
this.texture.destroy()
this.texture = null
}
this.emit('destroy')
}
/** 保存视差图数据 */
saveData() {
return {
visible: this.visible,
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
x: this.x,
y: this.y,
}
}
/** 光线采样模式映射表(字符串 -> 着色器中的采样模式代码) */
static lightSamplingModes = {raw: 0, global: 1, anchor: 2, ambient: 3}
}
// ******************************** 场景瓦片地图类 ********************************
class SceneTilemap {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 瓦片地图可见性
* @type {boolean}
*/ visible
/** 瓦片地图预设数据ID
* @type {string}
*/ presetId
/** 瓦片地图独立变量ID
* @type {string}
*/ selfVarId
/** 瓦片地图名称
* @type {string}
*/ name
/** 瓦片地图图层
* @type {string}
*/ layer
/** 瓦片地图排序优先级
* @type {number}
*/ order
/** 瓦片地图光照采样模式
* @type {string}
*/ light
/** 瓦片地图混合模式
* @type {string}
*/ blend
/** 瓦片地图水平位置
* @type {number}
*/ x
/** 瓦片地图垂直位置
* @type {number}
*/ y
/** 瓦片地图的宽度
* @type {number}
*/ width
/** 瓦片地图的高度
* @type {number}
*/ height
/** 瓦片地图水平锚点
* @type {number}
*/ anchorX
/** 瓦片地图垂直锚点
* @type {number}
*/ anchorY
/** 瓦片地图水平偏移位置
* @type {number}
*/ offsetX
/** 瓦片地图垂直偏移位置
* @type {number}
*/ offsetY
/** 瓦片地图水平视差系数
* @type {number}
*/ parallaxFactorX
/** 瓦片地图垂直视差系数
* @type {number}
*/ parallaxFactorY
/** 瓦片地图不透明度
* @type {number}
*/ opacity
/** 图块开始位置X
* @type {number}
*/ tileStartX
/** 图块开始位置Y
* @type {number}
*/ tileStartY
/** 图块结束位置X
* @type {number}
*/ tileEndX
/** 图块结束位置Y
* @type {number}
*/ tileEndY
/** 瓦片地图的图块列表
* @type {Uint32Array}
*/ tiles
/** 瓦片地图的具体图块数据映射表
* @type {Object}
*/ tileData
/** 瓦片地图的图块组映射表
* @type {Object}
*/ tilesetMap
/** 瓦片地图的纹理映射表
* @type {Object}
*/ textures
/** 瓦片地图的更新器模块列表
* @type {ModuleList}
*/ updaters
/** 瓦片地图的事件映射表
* @type {Object}
*/ events
/** 瓦片地图的脚本管理器
* @type {Script}
*/ script
/** 瓦片地图的父级对象
* @type {SceneParallaxManager|null}
*/ parent
/** 已开始状态
* @type {boolean}
*/ started
/**
* 场景瓦片地图对象
* @param {SceneContext} scene 场景上下文对象
* @param {Object} tilemap 场景中预设的瓦片地图数据
*/
constructor(scene, tilemap) {
this.scene = scene
this.visible = tilemap.visible ?? true
this.presetId = tilemap.presetId
this.selfVarId = tilemap.selfVarId ?? ''
this.name = tilemap.name
this.layer = tilemap.layer
this.order = tilemap.order
this.light = tilemap.light
this.blend = tilemap.blend
this.x = tilemap.x
this.y = tilemap.y
this.width = tilemap.width
this.height = tilemap.height
this.anchorX = tilemap.anchorX
this.anchorY = tilemap.anchorY
this.offsetX = tilemap.offsetX
this.offsetY = tilemap.offsetY
this.parallaxFactorX = tilemap.parallaxFactorX
this.parallaxFactorY = tilemap.parallaxFactorY
this.opacity = tilemap.opacity
this.tileStartX = tilemap.tileStartX ?? Math.round(this.x - this.width * this.anchorX)
this.tileStartY = tilemap.tileStartY ?? Math.round(this.y - this.height * this.anchorY)
this.tileEndX = tilemap.tileEndX ?? this.tileStartX + this.width
this.tileEndY = tilemap.tileEndY ?? this.tileStartY + this.height
this.tiles = Codec.decodeTiles(tilemap.code, tilemap.width, tilemap.height)
this.tileData = {0: null}
this.imageData = {0: null}
this.tilesetMap = tilemap.tilesetMap
this.textures = {}
this.updaters = new ModuleList()
this.events = tilemap.events
this.script = Script.create(this, tilemap.scripts)
this.parent = null
this.started = false
// 创建键值相反的图块组映射表
this.createReverseMap(tilemap.tilesetMap)
// 创建所有图块数据
this.createAllTileData()
// 加载纹理并创建图块数据
this.draw = Function.empty
this.loadTextures().then(() => {
this.createAllImageData()
delete this.draw
})
}
/**
* 获取指定位置的图块地形
* @param {number} x 场景X
* @param {number} y 场景Y
* @returns {number} 地形编码
*/
getTileTerrain(x, y) {
if (x >= this.tileStartX && x < this.tileEndX && y >= this.tileStartY && y < this.tileEndY) {
const tx = x - this.tileStartX
const ty = y - this.tileStartY
const tile = this.tiles[tx + ty * this.width]
return this.tileData[tile & 0xffffff00]?.terrain ?? 0
}
return 0
}
// 创建键值相反的图块组映射表
createReverseMap(tilesetMap) {
const reverseMap = {}
for (const [index, guid] of Object.entries(tilesetMap)) {
reverseMap[guid] = parseInt(index)
}
tilesetMap.reverseMap = reverseMap
}
/** 加载图块纹理 */
loadTextures() {
return new Promise(resolve => {
const tiles = this.tiles
const length = tiles.length
const textures = this.textures
// 如果是非空图块,同步加载图块组纹理
for (let i = 0; i < length; i++) {
const tile = tiles[i]
if (tile !== 0) {
this.loadTexture(tile, true)
}
}
// 不存在图块纹理,立即返回
const list = Object.values(textures)
if (list.length === 0) {
return resolve()
}
// 等待加载所有图块纹理
let loaded = 0
const callback = () => {
if (++loaded === list.length) {
// 全部纹理加载后完成Promise
if (this.textures === textures) {
return resolve()
}
}
}
for (const texture of list) {
// 侦听纹理加载完毕事件
texture.on('load', callback)
}
})
}
// 加载纹理
loadTexture(tile, sync, callback = null) {
const tileData = this.tileData[tile & 0xffffff00]
if (tileData) {
let texture
switch (tileData.type) {
case 'normal': {
const guid = tileData.tileset.image
texture = this.textures[guid]
if (guid && texture === undefined) {
texture = GL.createImageTexture(guid, {sync})
this.textures[guid] = texture
}
break
}
case 'auto': {
const guid = tileData.autoTile.image
texture = this.textures[guid]
if (guid && texture === undefined) {
texture = GL.createImageTexture(guid, {sync})
this.textures[guid] = texture
}
break
}
}
if (callback) {
texture?.on('load', callback)
}
}
}
// 创建所有图块数据
createAllTileData() {
const tiles = this.tiles
const length = tiles.length
for (let i = 0; i < length; i++) {
this.createTileData(tiles[i])
}
}
// 创建所有图像数据
createAllImageData() {
const tiles = this.tiles
const length = tiles.length
for (let i = 0; i < length; i++) {
this.createImageData(tiles[i])
}
}
// 创建图块数据
createTileData(tile) {
// 如果当前图块数据未创建
const dataId = tile & 0xffffff00
if (this.tileData[dataId] === undefined) {
const guid = this.tilesetMap[tile >> 24]
const tileset = Data.tilesets[guid]
// 如果存在图块组数据
if (tileset !== undefined) {
switch (tileset.type) {
case 'normal': {
const tx = tile >> 8 & 0xff
const ty = tile >> 16 & 0xff
const id = tx + ty * tileset.width
this.tileData[dataId] = {
x: tx,
y: ty,
type: 'normal',
tileset: tileset,
terrain: tileset.terrains[id],
tag: tileset.tags[id],
priority: tileset.priorities[id] + tileset.globalPriority,
}
return
}
case 'auto': {
const tx = tile >> 8 & 0xff
const ty = tile >> 16 & 0xff
const id = tx + ty * tileset.width
const autoTile = tileset.tiles[id]
if (!autoTile) break
const template = Data.autotiles[autoTile.template]
if (!template) break
this.tileData[dataId] = {
x: tx,
y: ty,
type: 'auto',
tileset: tileset,
terrain: tileset.terrains[id],
tag: tileset.tags[id],
priority: tileset.priorities[id] + tileset.globalPriority,
autoTile: autoTile,
template: template,
}
return
}
}
}
// 没能创建图块数据,使用null占位,避免再次进行创建
this.tileData[dataId] = null
}
}
// 创建图像数据
createImageData(tile) {
// 如果图像数据未创建
if (this.imageData[tile] === undefined) {
const tileData = this.tileData[tile & 0xffffff00]
// 如果存在图块组数据
if (tileData !== undefined) {
switch (tileData.type) {
case 'normal': {
// 如果存在纹理
const tileset = tileData.tileset
const texture = this.textures[tileset.image]
if (!texture) break
const scene = this.scene
const tw = scene.tileWidth
const th = scene.tileHeight
const sw = tileset.tileWidth
const sh = tileset.tileHeight
const sx = sw * tileData.x
const sy = sh * tileData.y
const dl = (tw - sw) / 2 + tileset.globalOffsetX
const dt = (th - sh) + tileset.globalOffsetY
const dr = dl + sw
const db = dt + sh
// 对图块纹理的采样坐标进行微调(避免一些渲染间隙)
let sl = (sx + 0.002) / texture.width
let sr = (sx + sw - 0.002) / texture.width
if (tile & 0b1) {
// 普通图块水平翻转
const temporary = sl
sl = sr
sr = temporary
}
const st = (sy + 0.002) / texture.height
const sb = (sy + sh - 0.002) / texture.height
const array = new Float32Array(11)
array[0] = texture.index
array[1] = tileData.priority
array[2] = 1
array[3] = dl
array[4] = dt
array[5] = dr
array[6] = db
array[7] = sl
array[8] = st
array[9] = sr
array[10] = sb
this.imageData[tile] = array
return
}
case 'auto': {
const tileset = tileData.tileset
const tx = tile >> 8 & 0xff
const ty = tile >> 16 & 0xff
const id = tx + ty * tileset.width
const autoTile = tileset.tiles[id]
const texture = this.textures[autoTile.image]
if (!texture) break
// 如果存在自动图块模板和纹理
const nodeId = tile & 0b111111
const node = tileData.template.nodes[nodeId]
if (!node) break
// 如果存在图块节点
const scene = this.scene
const tw = scene.tileWidth
const th = scene.tileHeight
const frames = node.frames
const length = frames.length
const sw = tileset.tileWidth
const sh = tileset.tileHeight
const dl = (tw - sw) / 2 + tileset.globalOffsetX
const dt = (th - sh) + tileset.globalOffsetY
const dr = dl + sw
const db = dt + sh
// 基础数据长度7,每一个动画帧加长度4
const array = new Float32Array(length * 4 + 7)
// 0:纹理索引,1:图块优先级,2:动画帧数量
array[0] = texture.index
array[1] = tileData.priority
array[2] = length
// 图块绘制的相对坐标
array[3] = dl
array[4] = dt
array[5] = dr
array[6] = db
const ox = autoTile.x
const oy = autoTile.y
const width = texture.width
const height = texture.height
// 遍历设置动画帧数据
for (let i = 0; i < length; i++) {
const index = i * 4 + 7
const frame = frames[i]
const sx = (ox + (frame & 0xff)) * sw
const sy = (oy + (frame >> 8)) * sh
const sl = (sx + 0.002) / width
const st = (sy + 0.002) / height
const sr = (sx + sw - 0.002) / width
const sb = (sy + sh - 0.002) / height
// 设置4个纹理采样坐标
array[index ] = sl
array[index + 1] = st
array[index + 2] = sr
array[index + 3] = sb
}
this.imageData[tile] = array
return
}
}
}
// 没能创建图块数据,使用null占位,避免再次进行创建
this.imageData[tile] = null
}
}
/**
* 设置图块
* @param {number} x 瓦片地图X
* @param {number} y 瓦片地图Y
* @param {string} tilesetId 图块组ID
* @param {number} tx 图块X
* @param {number} ty 图块Y
*/
setTile(x, y, tilesetId, tx, ty) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
const tileset = Data.tilesets[tilesetId]
if (tileset && tx >= 0 && tx < tileset.width && ty >= 0 && ty < tileset.height) {
const {tilesetMap} = this
const {reverseMap} = tilesetMap
let tileId = reverseMap[tilesetId]
if (tileId === undefined) outer: {
for (let i = 1; i < 256; i++) {
if (tilesetMap[i] === undefined) {
tilesetMap[i] = tilesetId
reverseMap[tilesetId] = i
tileId = i
break outer
}
}
return
}
const ti = x + y * this.width
const tile = tileId << 24 | ty << 16 | tx << 8
this.tiles[ti] = tile
this.createTileData(tile)
this.scene.updateTerrainObstacle(x, y)
this.updateSurroundingAutoTiles(x, y)
if (this.imageData[tile] === undefined) {
// 避免重复生成该图块数据
this.imageData[tile] = null
this.loadTexture(tile, false, () => {
this.imageData[tile] = undefined
this.createImageData(tile)
})
}
}
}
}
/**
* 删除图块
* @param {number} x 瓦片地图X
* @param {number} y 瓦片地图Y
*/
deleteTile(x, y) {
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
const ti = x + y * this.width
if (this.tiles[ti] !== 0) {
this.tiles[ti] = 0
this.scene.updateTerrainObstacle(x, y)
}
}
}
// 更新自动图块帧
updateSurroundingAutoTiles(x, y) {
const width = this.width
const height = this.height
const left = Math.max(x - 1, 0)
const top = Math.max(y - 1, 0)
const right = Math.min(x + 2, width)
const bottom = Math.min(y + 2, height)
const tiles = this.tiles
const tileData = this.tileData
for (let y = top; y < bottom; y++) {
for (let x = left; x < right; x++) {
if (x >= 0 && x < width && y >= 0 && y < height) {
const ti = x + y * width
const tile = tiles[ti]
const data = tileData[tile & 0xffffff00]
if (data?.type !== 'auto') continue
const template = data.template
const key = tile >> 8
const r = width - 1
const b = height - 1
const neighbor =
(x > 0 && key !== tiles[ti - 1 ] >> 8) + 1
| (x > 0 && y > 0 && key !== tiles[ti - 1 - width] >> 8) + 1 << 2
| ( y > 0 && key !== tiles[ti - width] >> 8) + 1 << 4
| (x < r && y > 0 && key !== tiles[ti + 1 - width] >> 8) + 1 << 6
| (x < r && key !== tiles[ti + 1 ] >> 8) + 1 << 8
| (x < r && y < b && key !== tiles[ti + 1 + width] >> 8) + 1 << 10
| ( y < b && key !== tiles[ti + width] >> 8) + 1 << 12
| (x > 0 && y < b && key !== tiles[ti - 1 + width] >> 8) + 1 << 14
const nodes = template.nodes
const length = nodes.length
let nodeIndex = 0
for (let i = 0; i < length; i++) {
const code = nodes[i].rule | neighbor
if (Math.max(
code & 0b11,
code >> 2 & 0b11,
code >> 4 & 0b11,
code >> 6 & 0b11,
code >> 8 & 0b11,
code >> 10 & 0b11,
code >> 12 & 0b11,
code >> 14 & 0b11,
) !== 0b11) {
nodeIndex = i
break
}
}
const nTile = key << 8 | nodeIndex
if (tiles[ti] !== nTile) {
tiles[ti] = nTile
this.createTileData(nTile)
if (this.imageData[nTile] === undefined) {
// 避免重复生成该图块数据
this.imageData[nTile] = null
this.loadTexture(nTile, false, () => {
this.imageData[nTile] = undefined
this.createImageData(nTile)
})
}
}
}
}
}
}
/**
* 更新场景瓦片地图
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
this.updaters.update(deltaTime)
}
/** 绘制场景瓦片地图 */
draw() {
const gl = GL
const vertices = gl.arrays[0].float32
const push = gl.batchRenderer.push
const response = gl.batchRenderer.response
const scene = this.scene
const imageData = this.imageData
const tiles = this.tiles
const width = this.width
const height = this.height
const frame = scene.animFrame
const tw = scene.tileWidth
const th = scene.tileHeight
const anchor = Scene.getParallaxAnchor(this)
const pw = width * tw
const ph = height * th
const ax = this.anchorX * pw
const ay = this.anchorY * ph
const ox = anchor.x - ax + this.offsetX
const oy = anchor.y - ay + this.offsetY
const sl = Camera.tileLeft - ox
const st = Camera.tileTop - oy
const sr = Camera.tileRight - ox
const sb = Camera.tileBottom - oy
const bx = Math.max(Math.floor(sl / tw), 0)
const by = Math.max(Math.floor(st / th), 0)
const ex = Math.min(Math.ceil(sr / tw), width)
const ey = Math.min(Math.ceil(sb / th), height)
// 使用队列渲染器进行批量渲染
gl.batchRenderer.setAttrSize(0)
gl.batchRenderer.setBlendMode(this.blend)
for (let y = by; y < ey; y++) {
for (let x = bx; x < ex; x++) {
const i = x + y * width
const tile = tiles[i]
const array = imageData[tile]
if (!array) continue
// 向渲染器添加纹理索引
push(array[0])
const fi = frame % array[2] * 4 + 7
const ox = x * tw
const oy = y * th
const dl = array[3] + ox
const dt = array[4] + oy
const dr = array[5] + ox
const db = array[6] + oy
const sl = array[fi]
const st = array[fi + 1]
const sr = array[fi + 2]
const sb = array[fi + 3]
// 从渲染器中获取顶点索引和采样器索引
const vi = response[0] * 5
const si = response[1]
vertices[vi ] = dl
vertices[vi + 1] = dt
vertices[vi + 2] = sl
vertices[vi + 3] = st
vertices[vi + 4] = si
vertices[vi + 5] = dl
vertices[vi + 6] = db
vertices[vi + 7] = sl
vertices[vi + 8] = sb
vertices[vi + 9] = si
vertices[vi + 10] = dr
vertices[vi + 11] = db
vertices[vi + 12] = sr
vertices[vi + 13] = sb
vertices[vi + 14] = si
vertices[vi + 15] = dr
vertices[vi + 16] = dt
vertices[vi + 17] = sr
vertices[vi + 18] = st
vertices[vi + 19] = si
}
}
// 如果顶点的尾部索引不为0(存在可绘制的图块)
const endIndex = gl.batchRenderer.getEndIndex()
if (endIndex !== 0) {
gl.alpha = this.opacity
const sl = Camera.scrollLeft
const st = Camera.scrollTop
const program = gl.tileProgram.use()
const modeMap = SceneTilemap.lightSamplingModes
const lightMode = this.light
const lightModeIndex = modeMap[lightMode]
const matrix = Matrix.instance.project(
gl.flip,
Camera.width,
Camera.height,
).translate(ox - sl, oy - st)
gl.bindVertexArray(program.vao)
gl.uniformMatrix3fv(program.u_Matrix, false, matrix)
gl.uniform1i(program.u_LightMode, lightModeIndex)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW, 0, endIndex * 5)
gl.batchRenderer.draw()
}
}
/**
* 调用瓦片地图事件
* @param {string} type 瓦片地图事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
event.triggerTilemap = this
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用瓦片地图事件和脚本
* @param {string} type 瓦片地图事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁瓦片地图 */
destroy() {
if (this.textures) {
// 销毁所有图块组纹理
for (const texture of Object.values(this.textures)) {
texture.refCount--
}
this.textures = null
}
this.emit('destroy')
}
/** 保存瓦片地图数据 */
saveData() {
return {
visible: this.visible,
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
x: this.x,
y: this.y,
tileStartX: this.tileStartX,
tileStartY: this.tileStartY,
tileEndX: this.tileEndX,
tileEndY: this.tileEndY,
tilesetMap: this.tilesetMap,
code: Codec.encodeTiles(this.tiles),
}
}
/** 静态 - 光线采样模式映射表(字符串 -> 着色器中的采样模式代码) */
static lightSamplingModes = {raw: 0, global: 1, ambient: 2}
}
// ******************************** 场景网格分区列表类 ********************************
class SceneGridCellList extends Array {
/** 场景网格分区大小
* @type {number}
*/ size
/** 场景网格分区移位
* @type {number}
*/ shift
/** 场景网格分区最小移位
* @type {number}
*/ minShift
/** 场景网格分区水平数量
* @type {number}
*/ width
/** 场景网格分区垂直数量
* @type {number}
*/ height
/** 场景网格分区导出列表
* @type {Array}
*/ exports
/** 场景网格分区列表 */
constructor() {
super()
this.size = 4
this.shift = 2
this.minShift = 2
this.width = 0
this.height = 0
this.exports = []
}
/**
* 优化中小型场景的最小分区大小
* @param {SceneContext} scene 场景上下文对象
*/
optimize(scene) {
const size = scene.width * scene.height
if (size <= 16384) {
this.size = 1
this.shift = 0
this.minShift = 0
} else if (size <= 65536) {
this.size = 2
this.shift = 1
this.minShift = 1
}
}
/**
* 调整场景网格分区的数量
* @param {SceneContext} scene 场景上下文对象
*/
resize(scene) {
// 根据场景大小调整分区数量
const cells = this.slice()
const width = Math.ceil(scene.width / this.size)
const height = Math.ceil(scene.height / this.size)
const length = width * height
this.width = width
this.height = height
this.length = length
for (let i = 0; i < length; i++) {
this[i] = []
}
// 重新添加角色到网格中
for (const cell of cells) {
for (const actor of cell) {
actor.cellId = -1
this.append(actor)
}
}
}
/**
* (尝试)扩大网格
* @param {SceneContext} scene 场景上下文对象
* @param {number} colliderSize 角色碰撞器大小
*/
grow(scene, colliderSize) {
if (this.size < colliderSize && this.shift < 4) {
while (this.size < colliderSize && this.shift < 4) {
this.size = 1 << ++this.shift
}
this.resize(scene)
}
}
/**
* (尝试)缩小网格
* @param {SceneContext} scene 场景上下文对象
* @param {number} colliderSize 角色碰撞器大小
*/
shrink(scene, colliderSize) {
if (this.size / 2 >= colliderSize && this.shift > this.minShift) {
while (this.size / 2 >= colliderSize && this.shift > this.minShift) {
this.size = 1 << --this.shift
}
this.resize(scene)
}
}
/**
* 获取指定范围的分区列表
* @param {number} sx 起始X
* @param {number} sy 起始Y
* @param {number} ex 结束X
* @param {number} ey 结束Y
* @returns {Array[]}
*/
get(sx, sy, ex, ey) {
const left = Math.max(sx >> this.shift, 0)
const top = Math.max(sy >> this.shift, 0)
const right = Math.min(ex / this.size, this.width)
const bottom = Math.min(ey / this.size, this.height)
const exports = this.exports
const rowOffset = this.width
let count = 0
// 获取小块分区中的数据,避免全局遍历
for (let y = top; y < bottom; y++) {
for (let x = left; x < right; x++) {
exports[count++] = this[x + y * rowOffset]
}
}
exports.count = count
return exports
}
/**
* 更新场景对象的分区(根据对象的位置来分配)
* @param {Object} object 拥有场景坐标的对象
*/
update(object) {
let cellId = -1
const cellX = object.x >> this.shift
const cellY = object.y >> this.shift
if (this.isValidPosition(cellX, cellY)) {
cellId = cellX + cellY * this.width
}
if (object.cellId !== cellId) {
// 从旧的分区中删除对象
if (object.cellId !== -1) {
this[object.cellId].remove(object)
}
// 添加对象到新的分区
if (cellId !== -1) {
this[cellId].push(object)
}
object.cellId = cellId
}
}
/**
* 添加场景对象到管理器中
* @param {Object} object 拥有场景坐标的对象
*/
append(object) {
const cellX = object.x >> this.shift
const cellY = object.y >> this.shift
if (this.isValidPosition(cellX, cellY)) {
const cellId = cellX + cellY * this.width
this[cellId].push(object)
object.cellId = cellId
}
}
/**
* 从管理器中移除场景对象
* @param {Object} object 拥有场景坐标的对象
*/
remove(object) {
const cell = this[object.cellId]
if (cell !== undefined) {
cell.remove(object)
object.cellId = -1
}
}
// 检查分区的位置是否有效
isValidPosition(cellX, cellY) {
return cellX >= 0 && cellX < this.width && cellY >= 0 && cellY < this.height
}
}
// ******************************** 场景角色列表类 ********************************
class SceneActorList extends Array {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 场景角色分区列表
* @type {SceneGridCellList}
*/ cells
/** 场景角色预设数据表
* @type {Object}
*/ presets
/**
* 场景角色列表
* @param {SceneContext} scene 场景上下文对象
*/
constructor(scene) {
super()
this.scene = scene
this.cells = new SceneGridCellList()
this.presets = {}
}
/**
* 添加角色到管理器中
* @param {Actor} actor 场景角色实例
*/
append(actor) {
if (actor.parent === null) {
actor.parent = this
this.push(actor)
this.cells.append(actor)
this.scene.obstacles.append(actor)
this.scene.idMap.set(actor.presetId, actor)
if (this.scene.enabled) {
actor.autorun()
}
}
}
/**
* 从管理器中移除角色
* @param {Actor} actor 场景角色实例
*/
remove(actor) {
if (actor.parent === this) {
actor.parent = null
super.remove(actor)
this.cells.remove(actor)
this.scene.obstacles.remove(actor)
this.scene.idMap.delete(actor.presetId, actor)
}
}
/**
* 查找指定队伍的角色数量
* @param {string} teamId 队伍ID
* @returns {number} 角色数量
*/
count(teamId) {
let count = 0
const {length} = this
for (let i = 0; i < length; i++) {
if (this[i].teamId === teamId) {
count++
}
}
return count
}
/**
* 更新场景角色
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
let maxColliderSize = 0
let maxGridCellSize = 0
for (const actor of this) {
actor.update(deltaTime)
if (maxGridCellSize < actor.collider.size) {
if (maxColliderSize < actor.collider.size) {
maxColliderSize = actor.collider.size
}
if (actor.collider.weight !== 0) {
maxGridCellSize = actor.collider.size
}
}
}
// 设置最大的角色碰撞器大小
this.setMaxColliderSize(maxColliderSize)
// 设置最大的网格分区大小
this.setMaxGridCellSize(maxGridCellSize)
// 处理角色和场景碰撞
ActorCollider.handleActorCollisions()
ActorCollider.handleSceneCollisions()
this.updateGridPosAndCells()
}
/**
* 设置最大的角色碰撞器大小
* @param {number} size 大小
*/
setMaxColliderSize(size) {
if (this.scene.maxColliderHalf !== size / 2) {
this.scene.maxColliderHalf = size / 2
}
}
/**
* 设置最大的网格分区大小
* @param {number} size 大小
*/
setMaxGridCellSize(size) {
if (this.scene.maxGridCellSize !== size) {
if (this.scene.maxGridCellSize < size) {
this.cells.grow(this.scene, size)
} else {
this.cells.shrink(this.scene, size)
}
}
}
/** 更新场景角色网格位置和分区 */
updateGridPosAndCells() {
const {scene, cells, length} = this
const {obstacles} = scene
for (let i = 0; i < length; i++) {
const actor = this[i]
// 只有角色发生移动时,才更新区间
if (actor.collider.moved) {
actor.collider.moved = false
// 更新上一次的位置
actor.collider.updateLastPosition()
// 更新角色的网格位置
actor.updateGridPosition()
// 更新角色的场景分区
cells.update(actor)
// 更新角色的障碍区域
obstacles.update(actor)
}
}
}
/** 发送自动执行事件 */
autorun() {
for (const actor of this) {
actor.autorun()
}
}
/** 销毁管理器中的场景角色 */
destroy() {
let i = this.length
while (--i >= 0) {
this[i].destroy()
}
}
/** 保存场景角色列表数据 */
saveData() {
const data = []
const length = this.length
for (let i = 0; i < length; i++) {
const actor = this[i]
if (!actor.active) continue
// 保存全局角色或普通角色
data.push(
actor instanceof GlobalActor
? {globalId: actor.data.id}
: actor.saveData()
)
}
return data
}
/**
* 加载场景角色列表数据
* @param {Object[]} actors
*/
loadData(actors) {
const presets = this.presets
for (const savedData of actors) {
const {presetId, fileId} = savedData
if (presetId) {
// 加载预设角色
const data = presets[presetId]?.data
if (data) {
this.append(new Actor(data, savedData))
}
} else if (fileId) {
// 加载外部角色
const data = Data.actors[fileId]
if (data) {
this.append(new Actor(data, savedData))
}
} else {
// 加载全局角色
const actor = ActorManager.get(savedData.globalId)
if (actor) {
this.append(actor)
}
}
}
// 恢复库存引用
Inventory.reference()
}
}
// ******************************** 场景动画列表类 ********************************
class SceneAnimationList extends Array {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 场景动画预设数据表
* @type {Object}
*/ presets
/**
* 场景动画列表
* @param {SceneContext} scene 场景上下文对象
*/
constructor(scene) {
super()
this.scene = scene
this.presets = {}
}
/**
* 添加动画到管理器中
* @param {Animation} animation 动画对象实例
*/
append(animation) {
if (animation.parent === null) {
animation.parent = this
this.push(animation)
this.scene.idMap.set(animation.presetId, animation)
if (this.scene.enabled) {
animation.autorun?.()
}
}
}
/**
* 从管理器中移除动画
* @param {Animation} animation 动画对象实例
*/
remove(animation) {
if (animation.parent === this) {
animation.parent = null
super.remove(animation)
this.scene.idMap.delete(animation.presetId, animation)
}
}
/**
* 更新动画实例
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
for (const animation of this) {
animation.update(deltaTime)
}
}
/** 发送自动执行事件 */
autorun() {
for (const animation of this) {
animation.autorun?.()
}
}
/** 销毁管理器中的动画 */
destroy() {
for (const animation of this) {
animation.destroy()
}
}
/** 保存动画列表数据 */
saveData() {
const length = this.length
const data = []
for (let i = 0; i < length; i++) {
const animation = this[i]
if ('saveData' in animation) {
data.push(animation.saveData())
}
}
return data
}
/**
* 加载动画列表数据
* @param {Object[]} animations
*/
loadData(animations) {
const presets = this.presets
for (const savedData of animations) {
const preset = presets[savedData.presetId]
if (preset) {
const data = Data.animations[preset.animationId]
if (data) {
// 重新创建动画实例
savedData.events = preset.events
savedData.scripts = preset.scripts
const animation = new SceneAnimation(savedData, data)
animation.setMotion(savedData.motion)
animation.setAngle(savedData.angle)
this.append(animation)
}
}
}
}
}
// ******************************** 场景动画类 ********************************
class SceneAnimation extends Animation {
/** 场景动画预设数据ID
* @type {string}
*/ presetId
/** 场景动画独立变量ID
* @type {string}
*/ selfVarId
/** 场景动画名称
* @type {string}
*/ name
/** 场景动画水平位置
* @type {number}
*/ x
/** 场景动画垂直位置
* @type {number}
*/ y
/** 场景动画的更新器模块列表
* @type {ModuleList}
*/ updaters
/** 场景动画事件映射表
* @type {Object}
*/ events
/** 场景动画脚本管理器
* @type {Script}
*/ script
/** 已开始状态
* @type {boolean}
*/ started
/**
* 场景动画对象
* @param {Object} node 场景中预设的动画数据
* @param {AnimFile} data 动画文件数据
*/
constructor(node, data) {
super(data)
this.visible = node.visible ?? true
this.presetId = node.presetId
this.selfVarId = node.selfVarId ?? ''
this.rotatable = node.rotatable
this.name = node.name
this.x = node.x
this.y = node.y
this.scale = node.scale
this.speed = node.speed
this.opacity = node.opacity
this.priority = node.priority
this.updaters = new ModuleList()
this.events = node.events
this.script = Script.create(this, node.scripts)
this.started = false
this.setPosition(this)
}
/**
* 更新场景动画
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
super.update(deltaTime)
this.updaters.update(deltaTime)
}
/**
* 调用场景动画事件
* @param {string} type 场景动画事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用场景动画事件和脚本
* @param {string} type 场景动画事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁场景动画 */
destroy() {
super.destroy()
this.emit('destroy')
}
/** 保存场景动画数据 */
saveData() {
return {
visible: this.visible,
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
motion: this.motionName,
rotatable: this.rotatable,
x: this.x,
y: this.y,
angle: this.angle,
scale: this.scale,
speed: this.speed,
opacity: this.opacity,
priority: this.priority,
}
}
}
// ******************************** 场景触发器列表类 ********************************
class SceneTriggerList extends Array {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/**
* 场景触发器列表
* @param {SceneContext} scene 场景上下文对象
*/
constructor(scene) {
super()
this.scene = scene
}
/**
* 添加触发器到管理器中
* @param {Trigger} trigger 触发器实例
*/
append(trigger) {
if (trigger.parent === null) {
trigger.parent = this
trigger.autorun()
this.push(trigger)
}
}
/**
* 从管理器中移除触发器
* @param {Trigger} trigger 触发器实例
*/
remove(trigger) {
if (trigger.parent === this) {
trigger.parent = null
super.remove(trigger)
}
}
/**
* 更新触发器
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
for (const trigger of this) {
trigger.update(deltaTime)
}
}
/** 销毁管理器中的触发器 */
destroy() {
for (const trigger of this) {
trigger.destroy()
}
}
}
// ******************************** 场景区域列表类 ********************************
class SceneRegionList extends Array {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 场景区域预设数据ID
* @type {Object}
*/ presets
/**
* 场景区域列表
* @param {SceneContext} scene 场景上下文对象
*/
constructor(scene) {
super()
this.scene = scene
this.presets = {}
}
/**
* 添加区域到管理器中
* @param {SceneRegion} region 场景区域对象
*/
append(region) {
if (region.parent === null) {
region.parent = this
this.push(region)
this.scene.idMap.set(region.presetId, region)
if (this.scene.enabled) {
region.autorun()
}
}
}
/**
* 从管理器中移除区域
* @param {SceneRegion} region 场景区域对象
*/
remove(region) {
if (region.parent === this) {
region.parent = null
super.remove(region)
this.scene.idMap.delete(region.presetId, region)
}
}
/**
* 更新区域实例
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
for (const region of this) {
region.update(deltaTime)
}
}
/** 发送自动执行事件 */
autorun() {
for (const region of this) {
region.autorun()
}
}
/** 销毁管理器中的场景区域 */
destroy() {
for (const region of this) {
region.destroy()
}
}
/** 保存场景区域列表数据 */
saveData() {
const length = this.length
const data = new Array(length)
for (let i = 0; i < length; i++) {
data[i] = this[i].saveData()
}
return data
}
/**
* 加载场景区域列表数据
* @param {Object[]} regions
*/
loadData(regions) {
const presets = this.presets
for (const savedData of regions) {
const data = presets[savedData.presetId]
if (data) {
// 重新创建区域实例
savedData.events = data.events
savedData.scripts = data.scripts
this.append(new SceneRegion(savedData))
}
}
}
}
// ******************************** 场景区域类 ********************************
class SceneRegion {
/** 场景区域预设数据ID
* @type {string}
*/ presetId
/** 场景区域独立变量ID
* @type {string}
*/ selfVarId
/** 场景区域名称
* @type {string}
*/ name
/** 场景区域水平位置
* @type {number}
*/ x
/** 场景区域垂直位置
* @type {number}
*/ y
/** 场景区域中已进入角色列表
* @type {Array<Actor>}
*/ actors
/** 场景区域更新器模块列表
* @type {ModuleList}
*/ updaters
/** 场景区域事件映射表
* @type {Object}
*/ events
/** 场景区域脚本管理器
* @type {Script}
*/ script
/** 场景区域的父级对象
* @type {SceneRegionList|null}
*/ parent
/** 已开始状态
* @type {boolean}
*/ started
/**
* 场景区域对象
* @param {Object} data 场景中预设的区域数据
*/
constructor(data) {
this.presetId = data.presetId
this.selfVarId = data.selfVarId ?? ''
this.name = data.name
this.x = data.x
this.y = data.y
this.width = data.width
this.height = data.height
this.actors = []
this.updaters = new ModuleList()
this.events = data.events
this.script = Script.create(this, data.scripts)
this.parent = null
this.started = false
this.createEventUpdaters()
}
/**
* 更新场景区域
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
this.updaters.update(deltaTime)
}
// 获取随机位置
getRandomPosition(terrain = -1) {
if (!this.parent) return undefined
let x = 0
let y = 0
let count = 0
const terrains = this.parent.scene.terrainObstacles
do {
const l = this.x - this.width / 2
const r = this.x + this.width / 2
const t = this.y - this.height / 2
const b = this.y + this.height / 2
x = Math.randomBetween(l, r)
y = Math.randomBetween(t, b)
}
// 如果指定了地形
// 则最多循环1000次
while (
terrain !== -1 &&
terrains.get(Math.floor(x), Math.floor(y)) !== terrain &&
++count < 1000
)
return count < 1000 ? {x, y} : undefined
}
/** 创建区域事件更新器 */
createEventUpdaters() {
// 如果存在相关事件,才会创建更新器
const {playerenter, playerleave, actorenter, actorleave} = this.events
if (playerenter || playerleave || actorenter || actorleave) {
const selections = this.actors
this.updaters.push({
update: () => {
// 检测进入区域的角色
const x = this.x
const y = this.y
const wh = this.width / 2
const hh = this.height / 2
const left = x - wh
const top = y - hh
const right = x + wh
const bottom = y + hh
const scene = this.parent.scene
const cells = scene.actors.cells.get(left, top, right, bottom)
const count = cells.count
for (let i = 0; i < count; i++) {
const actors = cells[i]
const length = actors.length
for (let i = 0; i < length; i++) {
const actor = actors[i]
const {x, y} = actor
if (x >= left && x < right && y >= top && y < bottom && selections.append(actor)) {
// 触发角色进入区域事件
if (actor.active) {
if (playerenter && actor === Party.player) {
this.emit('playerenter', actor)
}
if (actorenter) {
this.emit('actorenter', actor)
}
}
}
}
}
// 检测离开区域的角色
let i = selections.length
while (--i >= 0) {
const actor = selections[i]
const {x, y} = actor
if (x < left || x >= right || y < top || y >= bottom) {
selections.splice(i, 1)
// 触发角色离开区域事件
if (actor.active) {
if (playerleave && actor === Party.player) {
this.emit('playerleave', actor)
}
if (actorleave) {
this.emit('actorleave', actor)
}
}
}
}
}
})
}
}
/**
* 调用区域事件
* @param {string} type 区域事件类型
* @param {Actor} [triggerActor] 事件触发角色
* @returns {EventHandler|undefined}
*/
callEvent(type, triggerActor) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
if (triggerActor) {
event.triggerActor = triggerActor
}
event.triggerRegion = this
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用区域事件和脚本
* @param {string} type 区域事件类型
* @param {Actor} [triggerActor] 事件触发角色
*/
emit(type, triggerActor) {
this.callEvent(type, triggerActor)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁场景区域 */
destroy() {
this.emit('destroy')
}
/** 保存区域数据 */
saveData() {
return {
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
x: this.x,
y: this.y,
width: this.width,
height: this.height,
}
}
}
// ******************************** 场景光源管理器类 ********************************
class SceneLightManager {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 场景光源预设数据表
* @type {Object}
*/ presets //:object
/** 场景光源群组列表
* @type {Array<Array<SceneLight>>}
*/ groups //:array
/**
* 场景光源管理器
* @param {SceneContext} scene 场景上下文对象
*/
constructor(scene) {
this.scene = scene
this.presets = {}
// 根据混合模式创建子列表
const max = []
const screen = []
const additive = []
const subtract = []
const groups = [max, screen, additive, subtract]
groups.max = max
groups.screen = screen
groups.additive = additive
groups.subtract = subtract
this.groups = groups
}
/**
* 添加场景光源到管理器中
* @param {SceneLight} light 场景光源实例
*/
append(light) {
if (light.parent === null) {
light.parent = this
this.groups[light.blend].push(light)
this.scene.idMap.set(light.presetId, light)
if (this.scene.enabled) {
light.autorun()
}
}
}
/**
* 从管理器中移除场景光源
* @param {SceneLight} light 场景光源实例
*/
remove(light) {
if (light.parent === this) {
light.parent = null
this.groups[light.blend].remove(light)
this.scene.idMap.delete(light.presetId, light)
}
}
/**
* 更新场景光源
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
for (const group of this.groups) {
for (const light of group) {
light.update(deltaTime)
}
}
}
/** 渲染环境光和场景光源到纹理中 */
render() {
// 绘制环境光
const gl = GL
const scene = this.scene
const ambient = scene.ambient
const ambientRed = ambient.red / 255
const ambientGreen = ambient.green / 255
const ambientBlue = ambient.blue / 255
const ambientDirect = ambient.direct
// 获取反射光纹理裁剪区域
const cx = gl.reflectedLightMap.clipX
const cy = gl.reflectedLightMap.clipY
const cw = gl.reflectedLightMap.clipWidth
const ch = gl.reflectedLightMap.clipHeight
// 绑定反射光纹理FBO
gl.bindFBO(gl.reflectedLightMap.fbo)
gl.setViewport(cx, cy, cw, ch)
gl.clearColor(ambientRed, ambientGreen, ambientBlue, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
// 获取可见光源
const queue = gl.arrays[1].uint16
const tw = scene.tileWidth
const th = scene.tileHeight
// 获取场景光源可见范围,并转换成图块坐标
const sl = Camera.lightLeft
const st = Camera.lightTop
const sr = Camera.lightRight
const sb = Camera.lightBottom
const ll = sl / tw
const lt = st / th
const lr = sr / tw
const lb = sb / th
const vs = tw / th
const groups = this.groups
const length = groups.length
let qi = 0
for (let gi = 0; gi < length; gi++) {
const group = groups[gi]
const length = group.length
for (let i = 0; i < length; i++) {
const light = group[i]
if (light.visible) {
const {x, y} = light
switch (light.type) {
case 'point': {
const rr = light.range / 2
const px = x < ll ? ll : x > lr ? lr : x
const py = y < lt ? lt : y > lb ? lb : y
// 如果点光源可见,添加群组和光源索引到绘制队列
if ((px - x) ** 2 + ((py - y) * vs) ** 2 < rr ** 2) {
queue[qi++] = gi
queue[qi++] = i
}
continue
}
case 'area': {
const ml = x + light.measureOffsetX
const mt = y + light.measureOffsetY
const mr = ml + light.measureWidth
const mb = mt + light.measureHeight
// 如果区域光源可见,添加群组和光源索引到绘制队列
if (ml < lr && mt < lb && mr > ll && mb > lt) {
queue[qi++] = gi
queue[qi++] = i
}
continue
}
}
}
}
}
// 绘制反射光
if (qi !== 0) {
const projMatrix = Matrix.instance.project(
gl.flip,
sr - sl,
sb - st,
)
.translate(-sl, -st)
.scale(tw, th)
// 按队列顺序绘制所有可见光源
for (let i = 0; i < qi; i += 2) {
groups[queue[i]][queue[i + 1]].draw(projMatrix, 1)
}
}
// 重置视口并
gl.resetViewport()
// 计算直射光颜色
const directRed = ambientRed * ambientDirect
const directGreen = ambientGreen * ambientDirect
const directBlue = ambientBlue * ambientDirect
// 避免使用直射光纹理
gl.bindTexture(gl.TEXTURE_2D, null)
// 绑定直射光纹理FBO
gl.bindFBO(gl.directLightMap.fbo)
gl.clearColor(directRed, directGreen, directBlue, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
// 绘制直射光
if (qi !== 0) {
const sl = Camera.scrollLeft
const st = Camera.scrollTop
const sr = Camera.scrollRight
const sb = Camera.scrollBottom
const projMatrix = Matrix.instance.project(
gl.flip,
sr - sl,
sb - st,
)
.translate(-sl, -st)
.scale(tw, th)
// 按队列顺序绘制所有可见光源
for (let i = 0; i < qi; i += 2) {
const light = groups[queue[i]][queue[i + 1]]
light.draw(projMatrix, light.direct)
}
}
// 解除FBO绑定
gl.unbindFBO()
}
/** 发送自动执行事件 */
autorun() {
for (const group of this.groups) {
for (const light of group) {
light.autorun()
}
}
}
/** 销毁管理器中的场景光源 */
destroy() {
for (const group of this.groups) {
for (const light of group) {
light.destroy()
}
}
}
/** 保存场景光源列表数据 */
saveData() {
const data = []
for (const group of this.groups) {
for (const light of group) {
if (light.presetId !== '') {
data.push(light.saveData())
}
}
}
return data
}
/**
* 加载场景光源列表数据
* @param {Object[]} lights
*/
loadData(lights) {
const presets = this.presets
for (const savedData of lights) {
const data = presets[savedData.presetId]
if (data) {
// 重新创建光源实例
savedData.events = data.events
savedData.scripts = data.scripts
this.append(new SceneLight(savedData))
}
}
}
}
// ******************************** 场景光源类 ********************************
class SceneLight {
/** 场景光源可见性
* @type {boolean}
*/ visible
/** 场景光源预设数据ID
* @type {string}
*/ presetId
/** 场景光源独立变量ID
* @type {string}
*/ selfVarId
/** 场景光源名称
* @type {string}
*/ name
/** 场景光源类型
* @type {string}
*/ type
/** 场景光源混合模式
* @type {string}
*/ blend
/** 场景光源水平位置
* @type {number}
*/ x
/** 场景光源垂直位置
* @type {number}
*/ y
/** 点光源照亮范围(直径)
* @type {number}
*/ range
/** 点光源强度(0-1)
* @type {number}
*/ intensity
/** 光线颜色-红(0-255)
* @type {number}
*/ red
/** 光线颜色-绿(0-255)
* @type {number}
*/ green
/** 光线颜色-蓝(0-255)
* @type {number}
*/ blue
/** 直射率(0-1)
* @type {number}
*/ direct
/** 区域光源锚点水平偏移
* @type {number}
*/ anchorOffsetX
/** 区域光源锚点垂直偏移
* @type {number}
*/ anchorOffsetY
/** 区域光源测量外接矩形水平偏移
* @type {number}
*/ measureOffsetX
/** 区域光源测量外接矩形垂直偏移
* @type {number}
*/ measureOffsetY
/** 区域光源测量外接矩形宽度
* @type {number}
*/ measureWidth
/** 区域光源测量外接矩形高度
* @type {number}
*/ measureHeight
/** 区域光源图像纹理
* @type {ImageTexture|null}
*/ texture
/** 场景光源更新器模块列表
* @type {ModuleList}
*/ updaters
/** 场景光源事件映射表
* @type {Object}
*/ events
/** 场景光源脚本管理器
* @type {Script}
*/ script
/** 场景光源是否已经移动
* @type {boolean}
*/ moved
/** 场景光源的父级对象
* @type {SceneLightManager|null}
*/ parent
/** 已开始状态
* @type {boolean}
*/ started
_mask //:string
_anchorX //:number
_anchorY //:number
_width //:number
_height //:number
_angle //:number
/**
* 场景光源对象
* @param {Object} light 场景中预设的光源数据
*/
constructor(light = SceneLight.data) {
this.visible = light.visible ?? true
this.presetId = light.presetId
this.selfVarId = light.selfVarId ?? ''
this.name = light.name
this.type = light.type
this.blend = light.blend
this.x = light.x
this.y = light.y
switch (this.type) {
case 'point':
// 加载点光源属性
this.range = light.range
this.intensity = light.intensity
break
case 'area':
// 加载区域光源属性
this.texture = null
this.mask = light.mask
this.anchorX = light.anchorX
this.anchorY = light.anchorY
this.width = light.width
this.height = light.height
this.angle = light.angle
break
}
this.red = light.red
this.green = light.green
this.blue = light.blue
this.direct = light.direct
this.updaters = new ModuleList()
this.events = light.events
this.script = Script.create(this, light.scripts)
this.parent = null
this.started = false
SceneLight.latest = this
}
/**
* 蒙版图像文件ID
* @type {string}
*/
get mask() {
return this._mask
}
set mask(value) {
if (this._mask !== value) {
this._mask = value
// 销毁上一次的纹理
if (this.texture) {
this.texture.destroy()
this.texture = null
}
if (value) {
this.texture = new ImageTexture(value)
}
}
}
/**
* 区域光源锚点X
* @type {number}
*/
get anchorX() {
return this._anchorX
}
set anchorX(value) {
this._anchorX = value
this.moved = true
}
/**
* 区域光源锚点Y
* @type {number}
*/
get anchorY() {
return this._anchorY
}
set anchorY(value) {
this._anchorY = value
this.moved = true
}
/**
* 区域光源宽度
* @type {number}
*/
get width() {
return this._width
}
set width(value) {
this._width = value
this.moved = true
}
/**
* 区域光源高度
* @type {number}
*/
get height() {
return this._height
}
set height(value) {
this._height = value
this.moved = true
}
/**
* 区域光源角度(degrees)
* @type {number}
*/
get angle() {
return Math.degrees(this._angle)
}
set angle(degrees) {
// 转换为弧度(将会频繁使用)
this._angle = Math.radians(degrees)
this.moved = true
}
/**
* 更新场景光源
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
// 更新模块
this.updaters.update(deltaTime)
// 如果区域光发生移动,重新测量位置
if (this.moved) {
this.moved = false
this.measure()
}
}
/**
* 绘制场景光源
* @param {Matrix} projMatrix 投影矩阵
* @param {number} opacity 不透明度
*/
draw(projMatrix, opacity) {
switch (this.type) {
case 'point':
return this.drawPointLight(projMatrix, opacity)
case 'area':
return this.drawAreaLight(projMatrix, opacity)
}
}
/**
* 绘制点光源
* @param {Matrix} projMatrix 投影矩阵
* @param {number} opacity 不透明度
*/
drawPointLight(projMatrix, opacity) {
const gl = GL
const vertices = gl.arrays[0].float32
const r = this.range / 2
const ox = this.x
const oy = this.y
const dl = ox - r
const dt = oy - r
const dr = ox + r
const db = oy + r
vertices[0] = dl
vertices[1] = dt
vertices[2] = 0
vertices[3] = 0
vertices[4] = dl
vertices[5] = db
vertices[6] = 0
vertices[7] = 1
vertices[8] = dr
vertices[9] = db
vertices[10] = 1
vertices[11] = 1
vertices[12] = dr
vertices[13] = dt
vertices[14] = 1
vertices[15] = 0
gl.blend = this.blend
const program = gl.lightProgram.use()
const red = this.red * opacity / 255
const green = this.green * opacity / 255
const blue = this.blue * opacity / 255
const intensity = this.intensity
gl.bindVertexArray(program.vao.a110)
gl.vertexAttrib4f(program.a_LightColor, red, green, blue, intensity)
gl.uniformMatrix3fv(program.u_Matrix, false, projMatrix)
gl.uniform1i(program.u_LightMode, 0)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW, 0, 16)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
}
/**
* 绘制区域光源
* @param {Matrix} projMatrix 投影矩阵
* @param {number} opacity 不透明度
*/
drawAreaLight(projMatrix, opacity) {
const texture = this.texture
if (texture?.complete === false) {
return
}
const gl = GL
const vertices = gl.arrays[0].float32
const ox = this.x
const oy = this.y
const dl = ox - this.anchorOffsetX
const dt = oy - this.anchorOffsetY
const dr = dl + this.width
const db = dt + this.height
vertices[0] = dl
vertices[1] = dt
vertices[2] = 0
vertices[3] = 0
vertices[4] = dl
vertices[5] = db
vertices[6] = 0
vertices[7] = 1
vertices[8] = dr
vertices[9] = db
vertices[10] = 1
vertices[11] = 1
vertices[12] = dr
vertices[13] = dt
vertices[14] = 1
vertices[15] = 0
gl.blend = this.blend
const program = gl.lightProgram.use()
const mode = texture !== null ? 1 : 2
const red = this.red * opacity / 255
const green = this.green * opacity / 255
const blue = this.blue * opacity / 255
const matrix = gl.matrix
.set(projMatrix)
.rotateAt(ox, oy, this._angle)
gl.bindVertexArray(program.vao.a110)
gl.vertexAttrib4f(program.a_LightColor, red, green, blue, 0)
gl.uniformMatrix3fv(program.u_Matrix, false, matrix)
gl.uniform1i(program.u_LightMode, mode)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STREAM_DRAW, 0, 16)
gl.bindTexture(gl.TEXTURE_2D, texture?.base.glTexture)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
}
/** 测量区域光源的外接矩形(用于做绘制条件判断) */
measure() {
// 如果类型不是区域光源,返回
if (this.type !== 'area') return
const width = this.width
const height = this.height
const anchorOffsetX = width * this.anchorX
const anchorOffsetY = height * this.anchorY
const a = -anchorOffsetX
const b = -anchorOffsetY
const c = a + width
const d = b + height
const angle = this._angle
const angle1 = Math.atan2(b, a) + angle
const angle2 = Math.atan2(b, c) + angle
const angle3 = Math.atan2(d, c) + angle
const angle4 = Math.atan2(d, a) + angle
const distance1 = Math.sqrt(a * a + b * b)
const distance2 = Math.sqrt(c * c + b * b)
const distance3 = Math.sqrt(c * c + d * d)
const distance4 = Math.sqrt(a * a + d * d)
const x1 = Math.cos(angle1) * distance1
const x2 = Math.cos(angle2) * distance2
const x3 = Math.cos(angle3) * distance3
const x4 = Math.cos(angle4) * distance4
const y1 = Math.sin(angle1) * distance1
const y2 = Math.sin(angle2) * distance2
const y3 = Math.sin(angle3) * distance3
const y4 = Math.sin(angle4) * distance4
this.anchorOffsetX = anchorOffsetX
this.anchorOffsetY = anchorOffsetY
this.measureOffsetX = Math.min(x1, x2, x3, x4)
this.measureOffsetY = Math.min(y1, y2, y3, y4)
this.measureWidth = Math.max(Math.abs(x1 - x3), Math.abs(x2 - x4))
this.measureHeight = Math.max(Math.abs(y1 - y3), Math.abs(y2 - y4))
}
/**
* 移动场景光源
* @param {Array<string, number>} properties 光源属性词条
* @param {string} easingId 过渡曲线ID
* @param {number} duration 持续时间(毫秒)
*/
move(properties, easingId, duration) {
// 转换属性词条的数据结构
const propEntries = Object.entries(properties)
// 允许多个过渡同时存在且不冲突
const {updaters} = this
let transitions = updaters.get('move')
// 如果上一次的移动光源过渡未结束,获取过渡更新器列表
if (transitions) {
let ti = transitions.length
while (--ti >= 0) {
// 获取单个过渡更新器,检查属性词条
const updater = transitions[ti]
const entries = updater.entries
let ei = entries.length
while (--ei >= 0) {
const key = entries[ei][0]
for (const property of propEntries) {
// 从上一次过渡的属性中删除与当前过渡重复的属性
if (property[0] === key) {
entries.splice(ei, 1)
if (entries.length === 0) {
transitions.splice(ti, 1)
}
break
}
}
}
}
}
// 如果存在过渡
if (duration > 0) {
if (!transitions) {
// 如果不存在过渡更新器列表,新建一个
transitions = this.transitions = new ModuleList()
updaters.set('move', transitions)
}
const entries = []
const map = SceneLight.filters[this.type]
for (const [key, end] of propEntries) {
// 过滤掉与当前光源类型不匹配的属性
if (!map[key]) continue
const start = this[key]
entries.push([key, start, end])
}
let elapsed = 0
const easing = Easing.get(easingId)
// 创建更新器并添加到过渡更新器列表中
const updater = transitions.add({
entries: entries,
update: deltaTime => {
elapsed += deltaTime
const time = easing.map(elapsed / duration)
for (const [key, start, end] of entries) {
this[key] = start * (1 - time) + end * time
}
// 如果过渡结束,延迟移除更新器
if (elapsed >= duration) {
Callback.push(() => {
transitions.remove(updater)
// 如果过渡更新器列表为空,删除它
if (transitions.length === 0) {
updaters.delete('move')
}
})
}
}
})
} else {
// 直接设置光源属性
const map = SceneLight.filters[this.type]
for (const [key, value] of propEntries) {
// 过滤掉与当前光源类型不匹配的属性
if (!map[key]) continue
this[key] = value
}
// 如果存在过渡更新器列表并为空,删除它
if (transitions?.length === 0) {
updaters.deleteDelay('move')
}
}
}
/**
* 调用场景光源事件
* @param {string} type 场景光源事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
event.triggerLight = this
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用场景光源事件和脚本
* @param {string} type 场景光源事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁场景光源 */
destroy() {
if (this.texture) {
this.texture.destroy()
this.texture = null
}
this.emit('destroy')
}
/** 保存场景光源数据 */
saveData() {
switch (this.type) {
case 'point':
return {
visible: this.visible,
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
type: this.type,
blend: this.blend,
x: this.x,
y: this.y,
range: this.range,
intensity: this.intensity,
red: this.red,
green: this.green,
blue: this.blue,
}
case 'area':
return {
visible: this.visible,
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
type: this.type,
blend: this.blend,
x: this.x,
y: this.y,
mask: this.mask,
anchorX: this.anchorX,
anchorY: this.anchorY,
width: this.width,
height: this.height,
angle: this.angle,
red: this.red,
green: this.green,
blue: this.blue,
}
}
}
/**
* 最新创建光源
* @type {SceneLight|undefined}
*/
static latest
// 默认光源预设数据
static data = {
presetId: '',
name: '',
type: 'point',
blend: 'screen',
x: 0,
y: 0,
range: 4,
intensity: 0,
red: 255,
green: 255,
blue: 255,
direct: 0,
events: {},
scripts: [],
}
// 属性过滤器[点光源,区域光源]
static filters = function IIFE() {
const T = true
const common = {x: T, y: T, red: T, green: T, blue: T}
const point = {...common, range: T, intensity: T}
const area = {...common, anchorX: T, anchorY: T, width: T, height: T, angle: T}
return {point, area}
}()
}
// ******************************** 场景粒子发射器列表类 ********************************
class SceneParticleEmitterList extends Array {
/** 场景上下文对象
* @type {SceneContext}
*/ scene
/** 粒子发射器预设数据表
* @type {Object}
*/ presets
/**
* 场景粒子发射器列表
* @param {SceneContext} scene
*/
constructor(scene) {
super()
this.scene = scene
this.presets = {}
}
/**
* 添加场景粒子发射器到管理器中
* @param {SceneParticleEmitter} emitter 场景粒子发射器
*/
append(emitter) {
if (emitter.parent === null) {
emitter.parent = this
this.push(emitter)
this.scene.idMap.set(emitter.presetId, emitter)
if (this.scene.enabled) {
emitter.autorun?.()
}
}
}
/**
* 从管理器中移除场景粒子发射器
* @param {SceneParticleEmitter} emitter 场景粒子发射器
*/
remove(emitter) {
if (emitter.parent === this) {
emitter.parent = null
super.remove(emitter)
this.scene.idMap.delete(emitter.presetId, emitter)
}
}
/**
* 更新场景粒子发射器
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
for (const emitter of this) {
emitter.update(deltaTime)
}
}
/** 发送自动执行事件 */
autorun() {
for (const emitter of this) {
emitter.autorun?.()
}
}
/** 销毁管理器中的场景粒子发射器 */
destroy() {
const length = this.length
for (let i = 0; i < length; i++) {
this[i].destroy()
}
}
/** 保存场景粒子发射器列表数据 */
saveData() {
const length = this.length
const data = []
for (let i = 0; i < length; i++) {
const emitter = this[i]
if ('saveData' in emitter) {
data.push(emitter.saveData())
}
}
return data
}
/**
* 加载场景粒子发射器列表数据
* @param {Object[]} emitters
*/
loadData(emitters) {
const presets = this.presets
for (const savedData of emitters) {
const preset = presets[savedData.presetId]
if (preset) {
const data = Data.particles[preset.particleId]
if (data) {
// 重新创建粒子实例
savedData.events = preset.events
savedData.scripts = preset.scripts
this.append(new SceneParticleEmitter(savedData, data))
}
}
}
}
}
// ******************************** 场景粒子发射器类 ********************************
class SceneParticleEmitter extends ParticleEmitter {
/** 粒子发射器预设数据ID
* @type {string}
*/ presetId
/** 粒子发射器独立变量ID
* @type {string}
*/ selfVarId
/** 粒子发射器名称
* @type {string}
*/ name
/** 粒子发射器更新器模块列表
* @type {ModuleList}
*/ updaters
/** 粒子发射器事件映射表
* @type {Object}
*/ events
/** 粒子发射器脚本管理器
* @type {Script}
*/ script
/** 已开始状态
* @type {boolean}
*/ started
_x //:number
_y //:number
/**
* 场景粒子发射器
* @param {Object} node 场景中预设的粒子发射器数据
* @param {ParticleFile} data 粒子文件数据
*/
constructor(node, data) {
super(data)
this.visible = node.visible ?? true
this.presetId = node.presetId
this.selfVarId = node.selfVarId ?? ''
this.name = node.name
this.x = node.x
this.y = node.y
this.angle = node.angle
this.scale = node.scale
this.speed = node.speed
this.opacity = node.opacity
this.priority = node.priority
this.updaters = new ModuleList()
this.events = node.events
this.script = Script.create(this, node.scripts)
this.started = false
}
/**
* 粒子发射器水平位置
* @type {number}
*/
get x() {
return this._x
}
set x(value) {
this._x = value
this.startX = value * Scene.binding.tileWidth
}
/**
* 粒子发射器垂直位置
* @type {number}
*/
get y() {
return this._y
}
set y(value) {
this._y = value
this.startY = value * Scene.binding.tileHeight
}
/**
* 更新场景粒子发射器
* @param {number} deltaTime 增量时间(毫秒)
*/
update(deltaTime) {
const al = Camera.animationLeftT
const at = Camera.animationTopT
const ar = Camera.animationRightT
const ab = Camera.animationBottomT
const x = this._x
const y = this._y
// 如果粒子发射器可见,则发射新的粒子
if (x >= al && x < ar && y >= at && y < ab || this.alwaysEmit) {
this.emitParticles(deltaTime)
}
this.updateParticles(deltaTime)
this.updaters.update(deltaTime)
}
/**
* 调用场景粒子发射器事件
* @param {string} type 场景粒子发射器事件类型
* @returns {EventHandler|undefined}
*/
callEvent(type) {
const commands = this.events[type]
if (commands) {
const event = new EventHandler(commands)
event.selfVarId = this.selfVarId
return EventHandler.call(event, this.updaters)
}
}
/**
* 调用场景粒子发射器事件和脚本
* @param {string} type 场景粒子发射器事件类型
*/
emit(type) {
this.callEvent(type)
this.script.emit(type, this)
}
// 自动执行
autorun() {
if (this.started === false) {
this.started = true
this.emit('autorun')
}
}
/** 销毁场景粒子发射器 */
destroy() {
super.destroy()
this.emit('destroy')
}
/** 保存场景粒子发射器数据 */
saveData() {
return {
visible: this.visible,
presetId: this.presetId,
selfVarId: this.selfVarId,
name: this.name,
x: this.x,
y: this.y,
angle: this.angle,
scale: this.scale,
speed: this.speed,
opacity: this.opacity,
priority: this.priority,
}
}
}
// ******************************** 场景精灵渲染器类 ********************************
class SceneSpriteRenderer {
/** 场景对象列表组
* @type {Array<Array<Object>>}
*/ objectLists
/** 动画对象列表组
* @type {Array<Array<Object>>}
*/ animationLists
/** 缓存对象列表组
* @type {Array<Array<Object>>}
*/ cacheLists
/** 对象索引列表
* @type {Uint32Array}
*/ indices
/**
* 场景精灵渲染器
* @param {...Array} animationLists 可见动画列表组
*/
constructor(...animationLists) {
for (const list of animationLists) {
list.count = 0
}
this.animationLists = animationLists
this.cacheLists = animationLists.map(() => [])
this.indices = new Uint32Array(animationLists.length)
}
/**
* 重置所有数据
*/
reset() {
for (const animList of this.animationLists) {
for (let i = 0; i < animList.count; i++) {
animList[i] = null
}
animList.count = 0
}
}
/**
* 设置场景对象列表组
* @param {...Array} objectLists
*/
setObjectLists(...objectLists) {
this.objectLists = objectLists
}
/** 渲染场景中的可见对象 */
render() {
const {max, min, floor, ceil, round} = Math
const gl = GL
const lightmap = gl.reflectedLightMap
const scene = Scene.binding
const tw = scene.tileWidth
const th = scene.tileHeight
const tl = Camera.tileLeft
const tt = Camera.tileTop
const tr = Camera.tileRight
const tb = Camera.tileBottom
const ll = Camera.scrollLeft - lightmap.maxExpansionLeft
const lt = Camera.scrollTop - lightmap.maxExpansionTop
const lr = Camera.scrollRight + lightmap.maxExpansionRight
const lb = Camera.scrollBottom + lightmap.maxExpansionBottom
const lw = lr - ll
const lh = lb - lt
const ly = th / 2 / lh
const layers = gl.layers
const starts = gl.zeros
const ends = gl.arrays[1].uint32
const set = gl.arrays[2].uint32
const data = gl.arrays[3].float32
const datau = gl.arrays[3].uint32
let li = 0
let si = 2
let di = 0
// 获取对象层瓦片地图中可见图块的数据
const {doodads} = Scene.parallaxes
const dLength = doodads.length
for (let i = 0; i < dLength; i++) {
const tilemap = doodads[i]
if (!tilemap.visible) continue
const imageData = tilemap.imageData
if (imageData === null) continue
const tiles = tilemap.tiles
const width = tilemap.width
const height = tilemap.height
const anchor = Scene.getParallaxAnchor(tilemap)
const ax = tilemap.anchorX * width * tw
const ay = tilemap.anchorY * height * th
const ox = anchor.x - ax + tilemap.offsetX
const oy = anchor.y - ay + tilemap.offsetY
const bx = max(floor((tl - ox) / tw), 0)
const by = max(floor((tt - oy) / th), 0)
const ex = min(ceil((tr - ox) / tw), width)
const ey = min(ceil((tb - oy) / th), height)
const opacity = Math.round(tilemap.opacity * 255) << 8
for (let y = by; y < ey; y++) {
for (let x = bx; x < ex; x++) {
const ti = x + y * width
const tile = tiles[ti]
const array = imageData[tile]
if (!array) continue
const tp = array[1]
// 把网格底部作为图块锚点
const ax = (x + 0.5) * tw + ox
const ay = (y + 1) * th + oy
// 计算锚点在光照贴图中的位置
const px = (ax - ll) / lw
const py = (ay - lt + tp * th) / lh
// 根据锚点的Y坐标来计算优先级,并作为键
const key = max(0, min(0x3ffff, round(
py * 0x20000 + 0x10000
)))
// 光线采样锚点的Y坐标向上偏移了0.5格(网格中心)
const anchor = (
round(max(min(px, 1), 0) * 0xffff)
| round(max(min(py - ly, 1), 0) * 0xffff) << 16
)
datau[di ] = i
datau[di + 1] = tile
data[di + 2] = ax - tw / 2
data[di + 3] = ay - th
datau[di + 4] = anchor
datau[di + 5] = opacity
if (starts[key] === 0) {
starts[key] = si
layers[li++] = key
} else {
set[ends[key] + 1] = si
}
ends[key] = si
set[si++] = di
set[si++] = 0
di += 6
}
}
}
// 获取可见动画(角色、动画、触发器)
const convert2f = Scene.convert2f
const al = Camera.animationLeftT
const at = Camera.animationTopT
const ar = Camera.animationRightT
const ab = Camera.animationBottomT
const pFactor = th / lh
const animLists = this.animationLists
const cacheLists = this.cacheLists
const objectLists = this.objectLists
const aLength = animLists.length
for (let a = 0; a < aLength; a++) {
let count = 0
const animList = animLists[a]
const cacheList = cacheLists[a]
const objectList = objectLists[a]
const length = objectList.length
if (a === 0) {
// 遍历角色对象
for (let i = 0; i < length; i++) {
const actor = objectList[i]
const {x, y} = actor
// 如果动画在屏幕中可见或动画存在已激活粒子的情况
if (x >= al && x < ar && y >= at && y < ab && actor.visible) {
// 计算动画的锚点
const {x: ax, y: ay} = convert2f(x, y)
// 计算锚点在光照贴图中的位置
const px = (ax - ll) / lw
const py = (ay - lt) / lh
const p = actor.priority * pFactor
// 根据锚点的Y坐标来计算优先级,并作为键
const key = max(0, min(0x3ffff, round(
(py + p) * 0x20000 + 0x10000
)))
datau[di ] = 0x10000 | a
datau[di + 1] = count
if (starts[key] === 0) {
starts[key] = si
layers[li++] = key
} else {
set[ends[key] + 1] = si
}
ends[key] = si
set[si++] = di
set[si++] = 0
di += 2
actor.animationManager.activate(ax, ay, px, py)
cacheList[count++] = actor
} else if (actor.animationManager.existParticles) {
// 如果动画中存在粒子,更新粒子发射器的矩阵
const {x: ax, y: ay} = convert2f(x, y)
actor.animationManager.activate(ax, ay, 0, 0)
}
}
} else if (a <= 2) {
// 遍历动画相关对象
for (let i = 0; i < length; i++) {
const object = objectList[i]
const {animation} = object
if (animation === null) continue
const {x, y} = animation.position
// 如果动画在屏幕中可见或动画存在已激活粒子的情况
if (x >= al && x < ar && y >= at && y < ab && animation.visible) {
// 计算动画的锚点
const {x: ax, y: ay} = convert2f(x, y)
// 计算锚点在光照贴图中的位置
const px = (ax - ll) / lw
const py = (ay - lt) / lh
const p = animation.priority * pFactor
// 根据锚点的Y坐标来计算优先级,并作为键
const key = max(0, min(0x3ffff, round(
(py + p) * 0x20000 + 0x10000
)))
datau[di ] = 0x10000 | a
datau[di + 1] = count
if (starts[key] === 0) {
starts[key] = si
layers[li++] = key
} else {
set[ends[key] + 1] = si
}
ends[key] = si
set[si++] = di
set[si++] = 0
di += 2
animation.activate(ax, ay, px, py)
cacheList[count++] = object
} else if (animation.existParticles) {
// 如果动画中存在粒子,更新粒子发射器的矩阵
const {x: ax, y: ay} = convert2f(x, y)
animation.activate(ax, ay, 0, 0)
}
}
} else {
// 遍历粒子发射器
for (let i = 0; i < length; i++) {
const emitter = objectList[i]
const {x, y} = emitter
// 如果粒子在屏幕中可见或总是绘制的情况
if ((x >= al && x < ar && y >= at && y < ab || emitter.alwaysDraw) && emitter.visible) {
// 计算粒子的锚点
const {y: ay} = convert2f(x, y)
// 计算锚点在光照贴图中的位置
const py = (ay - lt) / lh
const p = emitter.priority * pFactor
// 根据锚点的Y坐标来计算优先级,并作为键
const key = max(0, min(0x3ffff, round(
(py + p) * 0x20000 + 0x10000
)))
datau[di ] = 0x10000 | a
datau[di + 1] = count
if (starts[key] === 0) {
starts[key] = si
layers[li++] = key
} else {
set[ends[key] + 1] = si
}
ends[key] = si
set[si++] = di
set[si++] = 0
di += 2
cacheList[count++] = emitter
}
}
}
// 擦除过期的动画对象引用
const end = animList.count
for (let i = count; i < end; i++) {
animList[i] = cacheList[i] = null
}
animList.count = count
}
// 绘制图像
if (li !== 0) {
const sl = Camera.scrollLeft
const st = Camera.scrollTop
const indices = this.indices.fill(0)
const vertices = gl.arrays[0].float32
const attributes = gl.arrays[0].uint32
const blend = gl.batchRenderer.setBlendMode
const push = gl.batchRenderer.push
const response = gl.batchRenderer.response
// 动画和对象层图块共用GL精灵程序进行绘制
const program = gl.spriteProgram.use()
const matrix = gl.matrix.project(
gl.flip,
Camera.width,
Camera.height,
).translate(-sl, -st)
// 绑定渲染器程序为当前程序
// 切换成粒子程序后自动恢复
gl.batchRenderer.bindProgram()
// 使用队列渲染器进行批量渲染
gl.batchRenderer.setAttrSize(8)
gl.bindVertexArray(program.vao)
gl.uniformMatrix3fv(program.u_Matrix, false, matrix)
const modeMap = Animation.lightSamplingModes
const frame = scene.animFrame
// 借助类型化数组对键(优先级)进行排序
const queue = new Uint32Array(layers.buffer, 0, li).sort()
for (let i = 0; i < li; i++) {
// 通过排序后的键来获取图块或动画
const key = queue[i]
let si = starts[key]
starts[key] = 0
do {
const di = set[si]
const code1 = datau[di]
const code2 = datau[di + 1]
if (code1 < 0x10000) {
// 如果第一个数据小于0x10000,则是图块索引
const tilemap = doodads[code1]
const light = modeMap[tilemap.light] << 16
const array = tilemap.imageData[code2]
blend(tilemap.blend)
push(array[0])
const fi = frame % array[2] * 4 + 7
const dx = data[di + 2]
const dy = data[di + 3]
const anchor = datau[di + 4]
const opacity = datau[di + 5]
const dl = array[3] + dx
const dt = array[4] + dy
const dr = array[5] + dx
const db = array[6] + dy
const sl = array[fi ]
const st = array[fi + 1]
const sr = array[fi + 2]
const sb = array[fi + 3]
const vi = response[0] * 8
// 参数压缩:纹理采样器ID,不透明度,光线采样模式
const param = response[1] | opacity | light
vertices [vi ] = dl
vertices [vi + 1] = dt
vertices [vi + 2] = sl
vertices [vi + 3] = st
attributes[vi + 4] = param
// 两个0x00ff00ff等于色调(0,0,0,0)
attributes[vi + 5] = 0x00ff00ff
attributes[vi + 6] = 0x00ff00ff
attributes[vi + 7] = anchor
vertices [vi + 8] = dl
vertices [vi + 9] = db
vertices [vi + 10] = sl
vertices [vi + 11] = sb
attributes[vi + 12] = param
attributes[vi + 13] = 0x00ff00ff
attributes[vi + 14] = 0x00ff00ff
attributes[vi + 15] = anchor
vertices [vi + 16] = dr
vertices [vi + 17] = db
vertices [vi + 18] = sr
vertices [vi + 19] = sb
attributes[vi + 20] = param
attributes[vi + 21] = 0x00ff00ff
attributes[vi + 22] = 0x00ff00ff
attributes[vi + 23] = anchor
vertices [vi + 24] = dr
vertices [vi + 25] = dt
vertices [vi + 26] = sr
vertices [vi + 27] = st
attributes[vi + 28] = param
attributes[vi + 29] = 0x00ff00ff
attributes[vi + 30] = 0x00ff00ff
attributes[vi + 31] = anchor
} else {
// 否则,是动画所在列表的索引
const li = code1 & 0xffff
const object = cacheLists[li][code2]
animLists[li][indices[li]++] = object
if (li === 0) {
object.animationManager.draw()
} else if (li <= 2) {
object.animation.draw()
} else {
gl.batchRenderer.draw()
object.draw()
}
}
} while ((si = set[si + 1]) !== 0)
}
gl.batchRenderer.draw()
gl.batchRenderer.unbindProgram()
gl.blend = 'normal'
}
}
}
// ******************************** 场景直射光渲染器类 ********************************
class SceneDirectLightRenderer {
// 渲染
render() {
GL.blend = 'additive'
GL.drawImage(GL.directLightMap, 0, 0, GL.width, GL.height)
}
}
// ******************************** 实体对象管理器 ********************************
const EntityManager = new class {
/** 实体对象ID映射表
* @type {Object}
*/ idMap = {}
/**
* 从映射表中获取对象
* @param {string} entityId 实体对象ID
* @returns {Object|undefined} 返回实体对象
*/
get(entityId) {
return this.idMap[entityId]
}
/**
* 添加对象到映射表中
* @param {Object} object 实体对象
*/
add(object) {
let {entityId} = object
if (entityId === '') {
// 如果对象不存在GUID,创建一个
do {entityId = GUID.generate64bit()}
while (entityId in this)
object.entityId = entityId
}
this.idMap[entityId] = object
}
/**
* 从映射表中移除对象
* @param {Object} object 实体对象
*/
remove(object) {
delete this.idMap[object.entityId]
}
/** 重置 */
// reset() {
// this.idMap = {}
// }
}