代码初始化

This commit is contained in:
Giao committed 2026-03-21 17:10:22 +08:00
1 parent 8160eca074
commit dffebce01d
43 files changed
+6383

No files matched your search

@@ -0,0 +1,666 @@
# -*- coding: utf-8 -*-
import math
import random
import re
import time
import mod.client.extraClientApi as clientApi
from mod.common.utils.mcmath import Matrix
from config.PlayConfig import PlayConfig
from utils.SafeEval import SafeEval
from .utils.CatmullRom import getCatmullromLerpFromKeyframe
from exception.NotPlayConfigException import NotPlayConfigException
from exception.NotClientSystemException import NotClientSystemException
compFactory = clientApi.GetEngineCompFactory()
levelId = clientApi.GetLevelId()
class Camera(object):
# 全局摄像机对象(用来判断当前客户端是否有摄像机正在播放)
globalCameraObj = None
def __init__(self, client, animationName, path='modconfigs/spring_camera_animations.json', cameraName='camera',
animationDict=None, speed=1.0):
"""
构造方法
Args:
client (ClientSystem): 客户端系统对象
animationName (str): 动画在文件中的名称
path (str, optional): 动画文件路径,需要包含 ".json"。默认值 'modconfigs/spring_camera_animations.json'
cameraName (str, optional): 动画 JSON 中的摄像机名称。默认值 'camera'
animationDict (dict, optional): 动画关键帧字典。默认值 None
speed (float, optional): 动画播放速度倍数。默认值 1.0
"""
if not isinstance(client, clientApi.GetClientSystemCls()):
raise NotClientSystemException("传入 Camera 内的 client 不是一个 ClientSystem")
# region 初始化变量
self.client = client
self.eval = SafeEval()
# 动画总时长
self.animationLength = None
# 动画旋转关键帧数组
self.animationRotationArray = None
# 动画位置关键帧数组
self.animationPositionArray = None
# 动画旋转关键帧数组的索引
self.rotationIndex = None
# 动画位置关键帧数组的索引
self.positionIndex = None
# 动画开始时间(单位:秒)
self.startAnimationTime = None
# 上一个帧的时间(单位:秒)
self.preAnimationTime = None
# 上一帧的旋转关键帧参数,形如:(0.0, [0, 180, 0], 'catmullrom')
self.preAnimationRotArgs = None
# 上一帧的位置关键帧参数,形如:(0.0, [0, 180, 0], 'catmullrom')
self.preAnimationPosArgs = None
# 动画开始前的摄像机旋转参数
self.rotationOrigin = None
# 动画开始前的摄像机锚点参数
self.positionOrigin = None
# play 配置对象
self.playConfig = PlayConfig()
# 是否正在播放摄像机动画
self.isPlaying = False
# 摄像机接口对象
self.cameraCompFactory = compFactory.CreateCamera(levelId)
# 绕 y 轴旋转的旋转矩阵
self.leftRotYMat = None
# 是否覆盖当前的位置和旋转(json 配置)
self.overridePreviousAnimation = False
# 动画循环状态:播放一次、停止于最后一帧、循环播放
self.loop = None
# 防止停不住
self.playTimer = None
# 前一帧设置的摄像机旋转角度(目前仅用于将当前帧设置的旋转角度传递给后续的 GameRenderTickEvent 事件)
self.beforeRot = None
# 是否正在暂停
self.isPausing = False
# 动画暂停开始时间
self.pauseStartTime = 0.0
# endregion
# 动画关键帧字典
if not animationDict:
animationDict = clientApi.GetModConfigJson(path)['animations'][animationName]
self.animationLength = animationDict.get('animation_length')
if not self.animationLength:
# 无时间的动画,默认动画时长 24 小时
self.animationLength = 86400.0
# 应用速度倍数
speed = 1.0 / speed
self.animationLength *= speed
self.overridePreviousAnimation = animationDict['override_previous_animation'] if animationDict.__contains__(
'override_previous_animation') else False
self.loop = animationDict['loop'] if animationDict.__contains__('loop') else False
if animationDict['bones'][cameraName].__contains__('rotation'):
animationRotationDict = animationDict['bones'][cameraName]['rotation']
# 解析只有单个关键帧的动画
if isinstance(animationRotationDict, list):
animationRotationDict = {'0.0': animationRotationDict}
# 动画旋转关键帧数组
self.animationRotationArray = sorted(animationRotationDict.items(), key=lambda item: float(item[0]))
self.animationRotationArray = [
(float(item[0]) * speed,
item[1]['post'] if isinstance(item[1], dict) else item[1],
item[1].get('lerp_mode') if isinstance(item[1], dict) else None)
for item in self.animationRotationArray
]
self._parseRotKeyframe(self.animationRotationArray)
if animationDict['bones'][cameraName].__contains__('position'):
animationPositionDict = animationDict['bones'][cameraName]['position']
# 解析只有单个关键帧的动画
if isinstance(animationPositionDict, list):
animationPositionDict = {'0.0': animationPositionDict}
# 动画位置关键帧数组
self.animationPositionArray = sorted(animationPositionDict.items(), key=lambda item: float(item[0]))
self.animationPositionArray = [
(float(item[0]) * speed,
item[1]['post'] if isinstance(item[1], dict) else item[1],
item[1].get('lerp_mode') if isinstance(item[1], dict) else None)
for item in self.animationPositionArray
]
self._parsePosKeyframe(self.animationPositionArray)
def _parseRotKeyframe(self, animationArray):
"""
转换旋转关键帧中的字符串
Args:
animationArray (list): 旋转关键帧数组
"""
rot = compFactory.CreateCamera(levelId).GetCameraRotation()
self._parseKeyframe(animationArray, rot)
def _parsePosKeyframe(self, animationArray):
"""
转换位置关键帧中的字符串
Args:
animationArray (list): 位置关键帧数组
"""
pos = compFactory.CreatePos(clientApi.GetLocalPlayerId()).GetFootPos()
self._parseKeyframe(animationArray, (pos[0] * 16.0, pos[1] * 16.0, pos[2] * 16.0))
def _parseKeyframe(self, animationArray, replaceThis):
"""
转换关键帧中的字符串
Args:
animationArray (list): 关键帧数组
replaceThis (tuple): 替换 this 的元组
"""
for i in xrange(0, len(animationArray)):
for j in xrange(0, len(animationArray[i][1])):
if not isinstance(animationArray[i][1][j], str):
continue
if 'this' in animationArray[i][1][j]:
animationArray[i][1][j] = animationArray[i][1][j].replace('this', format(replaceThis[j], '.16f'))
animationArray[i][1][j] = self.eval.evaluate(animationArray[i][1][j])
elif 'math.random' in animationArray[i][1][j]:
match = re.search(r'(-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?)', animationArray[i][1][j])
if match:
animationArray[i][1][j] = random.uniform(float(match.group(1)), float(match.group(3)))
def _indexOutEnd(self):
"""
防止下标越界
"""
# 如果数组下标越界,设置下标为最后一个元素
if self.animationRotationArray and self.rotationIndex >= len(self.animationRotationArray):
self.rotationIndex = len(self.animationRotationArray) - 1
if self.animationPositionArray and self.positionIndex >= len(self.animationPositionArray):
self.positionIndex = len(self.animationPositionArray) - 1
def _leftRotationMatrixY(self, angleDegrees):
"""
生成绕 Y 轴旋转指定角度的 3x3 旋转矩阵(左手坐标系)
Args:
angleDegrees (float): 旋转角度
Returns:
Matrix: 3x3 旋转矩阵
"""
# 将角度转换为弧度
angleRad = math.radians(angleDegrees)
# 计算余弦和正弦值
cosAngle = math.cos(angleRad)
sinAngle = math.sin(angleRad)
# 构建绕 Y 轴旋转矩阵(左手坐标系)
rotationMatrix = Matrix.Create([
[cosAngle, 0, -sinAngle],
[0, 1, 0, ],
[sinAngle, 0, cosAngle]
])
return rotationMatrix
def _leftRotationMatrixX(self, angleDegrees):
"""
生成绕 X 轴旋转指定角度的 3x3 旋转矩阵(左手坐标系)
Args:
angleDegrees (float): 旋转角度
Returns:
Matrix: 3x3 旋转矩阵
"""
# 将角度转换为弧度
angleRad = math.radians(angleDegrees)
# 计算余弦和正弦值
cosAngle = math.cos(angleRad)
sinAngle = math.sin(angleRad)
# 构建绕 X 轴旋转矩阵(左手坐标系)
rotationMatrix = Matrix.Create([
[1, 0, 0],
[0, cosAngle, sinAngle],
[0, -sinAngle, cosAngle]
])
return rotationMatrix
def _getNowTime(self):
"""
获取当前时间
"""
if self.isPausing and self.preAnimationTime is not None:
return self.preAnimationTime
return time.time()
def playFirstPerson(self, playAfterFunc=None):
"""
播放第一人称动画(允许拖拽,允许移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(True, True, True, False, True, playAfterFunc=playAfterFunc)
def playFirstPersonDisableMove(self, playAfterFunc=None):
"""
播放第一人称动画(允许拖拽,禁止移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(True, True, False, False, True, playAfterFunc=playAfterFunc)
def playFirstPersonDisableDrag(self, playAfterFunc=None):
"""
播放第一人称动画(禁止拖拽,允许移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(True, False, True, False, True, playAfterFunc=playAfterFunc)
def playFirstPersonDisableDragWithMove(self, playAfterFunc=None):
"""
播放第一人称动画(禁止拖拽,禁止移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(True, False, False, False, True, playAfterFunc=playAfterFunc)
def playThirdPerson(self, playAfterFunc=None):
"""
播放第三人称动画(允许拖拽,允许移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(False, True, True, playAfterFunc=playAfterFunc)
def playThirdPersonDisableMove(self, playAfterFunc=None):
"""
播放第三人称动画(允许拖拽,禁止移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(False, True, False, playAfterFunc=playAfterFunc)
def playThirdPersonDisableDrag(self, playAfterFunc=None):
"""
播放第三人称动画(禁止拖拽,允许移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(False, False, True, playAfterFunc=playAfterFunc)
def playThirdPersonDisableDragWithMove(self, playAfterFunc=None):
"""
播放第三人称动画(禁止拖拽,禁止移动)
Args:
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.play(False, False, False, playAfterFunc=playAfterFunc)
def play(self, isFirstPerson=True, isAllowDrag=True, isAllowMove=True, isPosFollowXRot=True, isPosFollowYRot=True,
isZRotForceReturn=True, isZRotStartOrigin=False,
isDelayOneFrame=False, isLockPerspective=False, tickPosFunc=None, tickRotFunc=None, playAfterFunc=None):
"""
播放摄像机动画
Args:
isFirstPerson (bool, optional): 是否是第一人称动画。如果是,那么 Y 轴旋转会减 180 度。默认值 False
isAllowDrag (bool, optional): 是否允许玩家拖拽视角。默认值 False
isAllowMove (bool, optional): 是否允许玩家移动。默认值 False
isPosFollowXRot (bool, optional): 摄像机空间坐标是否受 X 轴旋转影响(仅在第一人称下生效)。默认值 True
isPosFollowYRot (bool, optional): 摄像机空间坐标是否受 Y 轴旋转影响(仅在第一人称下生效)。默认值 True
isZRotForceReturn (bool, optional): 是否在动画播放完成后强制归零 Z 轴的旋转值。默认值 True
isZRotStartOrigin (bool, optional): 是否将动画播放前的 Z 轴旋转值作为初始的 Z 轴旋转值。置为 False 可防止玩家在受伤瞬间播放摄像机动画造成的 Z 轴旋转异常(json 配置 override_previous_animation 值为 True 时无效)。默认值 False
isDelayOneFrame (bool, optional): 是否延迟一帧再播放摄像机动画。默认值 True
isLockPerspective (bool, optional): 是否锁定并强制切换玩家视角。默认值 True
tickPosFunc (function, optional): 摄像机动画播放时,在每次设置摄像机位置前执行的函数。默认值 None
tickRotFunc (function, optional): 摄像机动画播放时,在每次设置摄像机旋转前执行的函数。默认值 None
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.playWithConfig(
PlayConfig(isFirstPerson, isAllowDrag, isAllowMove, isPosFollowXRot, isPosFollowYRot, isZRotForceReturn,
isZRotStartOrigin, isDelayOneFrame, isLockPerspective,
tickPosFunc, tickRotFunc, playAfterFunc))
def playWithConfig(self, config):
"""
使用播放配置对象播放摄像机动画
Args:
config (PlayConfig): 播放配置对象
"""
if not isinstance(config, PlayConfig):
raise NotPlayConfigException("传入 playWithConfig 内的 config 不是一个 PlayConfig")
if Camera.globalCameraObj:
# 强制停止正在播放的摄像机动画(全局)
Camera.globalCameraObj.stop(True)
# 开始监听
from .CameraListen import _enable
if not _enable:
self.client.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(),
"GameRenderTickEvent", self, self.GameRenderTickEvent, 10)
self.client.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(),
"HealthChangeClientEvent", self, self.HealthChangeClientEvent)
def realPlay():
# 初始化相关变量
self.playConfig = config
self.rotationIndex = 0
self.positionIndex = 0
operationComp = compFactory.CreateOperation(levelId)
# 不允许玩家移动
if not self.playConfig.isAllowMove:
operationComp.SetCanMove(False)
operationComp.SetCanJump(False)
operationComp.SetCanInair(False)
# 不允许玩家拖拽
if not self.playConfig.isAllowDrag:
operationComp.SetCanDrag(False)
# 不允许玩家切换视角
if self.playConfig.isLockPerspective:
operationComp.SetCanPerspective(False)
# 第三人称
if not self.playConfig.isFirstPerson:
if self.playConfig.isLockPerspective:
compFactory.CreatePlayerView(clientApi.GetLocalPlayerId()).SetPerspective(1)
self.cameraCompFactory.SetCameraDistanceFixed(True)
self.cameraCompFactory.DepartCamera()
self.cameraCompFactory.SetCameraOffset((0, 0.01, -4))
else:
if self.playConfig.isLockPerspective:
compFactory.CreatePlayerView(clientApi.GetLocalPlayerId()).SetPerspective(0)
if self.animationRotationArray:
self.preAnimationRotArgs = (float(self.animationRotationArray[self.rotationIndex][0]),
self.animationRotationArray[self.rotationIndex][1])
if self.animationPositionArray:
self.preAnimationPosArgs = (float(self.animationPositionArray[self.positionIndex][0]),
self.animationPositionArray[self.positionIndex][1])
self.preAnimationTime = self.startAnimationTime = time.time()
# 是否覆盖
if self.overridePreviousAnimation:
self.rotationOrigin = (0, 0, 0)
self.positionOrigin = (0, 0, 0)
else:
self.rotationOrigin = self.cameraCompFactory.GetCameraRotation()
self.positionOrigin = self.cameraCompFactory.GetCameraAnchor()
# 可防止玩家在受伤瞬间播放摄像机动画造成的 Z 轴旋转异常
if not self.playConfig.isZRotStartOrigin:
self.rotationOrigin = (self.rotationOrigin[0], self.rotationOrigin[1], 0.0)
# 绕 y 轴的旋转矩阵
self.leftRotYMat = self._leftRotationMatrixY(self.rotationOrigin[1])
# 一定要在最后给 isPlaying 赋值
Camera.globalCameraObj = self
self.isPlaying = True
if config.isDelayOneFrame:
# 延时一帧再播放(为了确保上一个动画完全停止,使得下一次获取初始数据时不受上次动画影响)
compGame = compFactory.CreateGame(levelId)
self.playTimer = compGame.AddTimer(0.0, realPlay)
else:
realPlay()
def destroyListen(self):
"""
销毁内置监听
"""
self.client.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(),
"GameRenderTickEvent", self, self.GameRenderTickEvent)
self.client.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(),
"HealthChangeClientEvent", self, self.HealthChangeClientEvent)
def stop(self, isStopLoop=False):
"""
停止摄像机动画
Args:
isStopLoop (bool, optional): 是否停止循环动画(仅对 json 中 loop 配置为 True 的动画有效)。默认值 False
"""
if self.playTimer: compFactory.CreateGame(levelId).CancelTimer(self.playTimer)
# 取消监听
from .CameraListen import _enable
if not _enable:
self.destroyListen()
self.isPlaying = False
self.isPausing = False
Camera.globalCameraObj = None
# region 摄像机还原
# 第三人称
if not self.playConfig.isFirstPerson:
if self.playConfig.isLockPerspective:
compFactory.CreatePlayerView(clientApi.GetLocalPlayerId()).SetPerspective(0)
self.cameraCompFactory.SetCameraDistanceFixed(False)
self.cameraCompFactory.UnDepartCamera()
self.cameraCompFactory.SetCameraOffset((0, 0, 0))
# 根据 loop 的值进行设置
nowCameraRotation = self.rotationOrigin
nowCameraPosition = self.positionOrigin
if self.loop == 'hold_on_last_frame' or self.loop == True or self.playConfig.isAllowDrag:
nowCameraRotation = self.cameraCompFactory.GetCameraRotation()
if not self.playConfig.isAllowDrag:nowCameraPosition = self.cameraCompFactory.GetCameraAnchor()
# 是否强制回正 Z 轴
if self.playConfig.isZRotForceReturn:
self.cameraCompFactory.SetCameraRotation((nowCameraRotation[0], nowCameraRotation[1], 0))
else:
self.cameraCompFactory.SetCameraRotation(nowCameraRotation)
self.cameraCompFactory.SetCameraAnchor(nowCameraPosition)
# endregion
# 判断是否循环播放
if self.loop == True and not isStopLoop:
self.playWithConfig(self.playConfig)
return
operationComp = compFactory.CreateOperation(levelId)
# 恢复玩家移动
operationComp.SetCanMove(True)
operationComp.SetCanJump(True)
operationComp.SetCanInair(True)
operationComp.SetCanDrag(True)
# 允许玩家切换视角
operationComp.SetCanPerspective(True)
# 动画停止播放后执行传入的回调函数
if (self.loop == True and isStopLoop) or (self.loop != True):
if callable(self.playConfig.playAfterFunc):
self.playConfig.playAfterFunc()
def pause(self, resumeTime=0):
# type: (float) -> None
"""
暂停摄像机动画
"""
if self.isPausing:
return
self.isPausing = True
self.pauseStartTime = time.time()
# 多少秒后恢复动画播放
if resumeTime > 0:
compFactory.CreateGame(levelId).AddTimer(resumeTime, self.resume)
def resume(self):
"""
继续播放摄像机动画
"""
if not self.isPausing:
return
self.isPausing = False
pauseTime = time.time() - self.pauseStartTime
self.startAnimationTime += pauseTime
self.preAnimationTime += pauseTime
def GameRenderTickEvent(self, event):
"""
每 Tick 触发
"""
if not self.isPlaying:
return
nowAnimationTime = self._getNowTime()
startToNowTime = nowAnimationTime - self.startAnimationTime # 当前时间距离动画开始的时间
preToNowTime = nowAnimationTime - self.preAnimationTime # 当前时间距离上一帧的时间
self.preAnimationTime = nowAnimationTime # 更新上一帧的时间为当前时间,为下一次计算做准备
# 如果自动画开始以来经过的时间,大于动画总时长,则动画结束
if startToNowTime > self.animationLength:
self.stop()
return
self._indexOutEnd()
# 如果自动画开始以来经过的时间,大于当前关键帧时间,说明关键帧索引应该增加
while self.animationRotationArray and self.rotationIndex < len(
self.animationRotationArray) and startToNowTime > float(
self.animationRotationArray[self.rotationIndex][0]):
# 1.0.3 修复上个关键帧没执行到结尾就计算下个关键帧所带来的问题
self.preAnimationRotArgs = (self.preAnimationRotArgs[0], self.animationRotationArray[self.rotationIndex][1])
self.rotationIndex += 1
while self.animationPositionArray and self.positionIndex < len(
self.animationPositionArray) and startToNowTime > float(
self.animationPositionArray[self.positionIndex][0]):
# 1.0.3 修复上个关键帧没执行到结尾就计算下个关键帧所带来的问题
self.preAnimationPosArgs = (self.preAnimationPosArgs[0], self.animationPositionArray[self.positionIndex][1])
self.positionIndex += 1
self._indexOutEnd()
# 计算位置
if self.animationPositionArray:
keyframePosTime = float(self.animationPositionArray[self.positionIndex][0]) - self.preAnimationPosArgs[
0] # 位置关键帧下个关键帧与上个关键帧之间的时间
# 只有时间间隔大于 0,才计算当前这一刻的值
if keyframePosTime > 0.0:
positionX = 0
positionY = 0
positionZ = 0
if self.animationPositionArray[self.positionIndex][2] == 'catmullrom':
positionX = getCatmullromLerpFromKeyframe(self.animationPositionArray, self.positionIndex,
startToNowTime, 0)
positionY = getCatmullromLerpFromKeyframe(self.animationPositionArray, self.positionIndex,
startToNowTime, 1)
positionZ = getCatmullromLerpFromKeyframe(self.animationPositionArray, self.positionIndex,
startToNowTime, 2)
else:
positionX = ((float(self.animationPositionArray[self.positionIndex][1][0]) -
self.preAnimationPosArgs[1][0]) / keyframePosTime * preToNowTime) + \
self.preAnimationPosArgs[1][0]
positionY = ((float(self.animationPositionArray[self.positionIndex][1][1]) -
self.preAnimationPosArgs[1][1]) / keyframePosTime * preToNowTime) + \
self.preAnimationPosArgs[1][1]
positionZ = ((float(self.animationPositionArray[self.positionIndex][1][2]) -
self.preAnimationPosArgs[1][2]) / keyframePosTime * preToNowTime) + \
self.preAnimationPosArgs[1][2]
pos = (
(1 if self.playConfig.isFirstPerson else -1) * positionX / 16.0,
positionY / 16.0 - 1.62,
(-1 if self.playConfig.isFirstPerson else 1) * positionZ / 16.0
)
posMat = Matrix.Create([
[pos[0]],
[pos[1]],
[pos[2]]
])
if self.playConfig.isFirstPerson:
# 第一人称
rotMax = Matrix.CreateEye(3)
nowCameraRotation = self.cameraCompFactory.GetCameraRotation()
deltX = (nowCameraRotation[0] - self.preAnimationRotArgs[1][0])
deltY = (nowCameraRotation[1] - self.preAnimationRotArgs[1][1])
# 注意矩阵相乘顺序
if self.playConfig.isPosFollowXRot:
rotMax = self._leftRotationMatrixX(deltX)
if self.playConfig.isPosFollowYRot:
rotMax = self._leftRotationMatrixY(deltY - 180) * rotMax
posMat = rotMax * posMat
else:
# 第三人称位置只受初始绕 y 轴旋转的影响
posMat = self.leftRotYMat * posMat
pos = (
posMat[0, 0] + self.positionOrigin[0],
posMat[1, 0] + self.positionOrigin[1],
posMat[2, 0] + self.positionOrigin[2]
)
# 是否不设置
isNotSet = False
if callable(self.playConfig.tickPosFunc):
isNotSet = self.playConfig.tickPosFunc(startToNowTime, pos)
if not isNotSet:
self.cameraCompFactory.SetCameraAnchor(pos)
self.preAnimationPosArgs = (startToNowTime, [positionX, positionY, positionZ])
# 计算旋转
if self.animationRotationArray:
keyframeRotTime = float(self.animationRotationArray[self.rotationIndex][0]) - self.preAnimationRotArgs[
0] # 旋转关键帧下个关键帧与上个关键帧之间的时间
# 只有时间间隔大于 0,才计算当前这一刻的值
if keyframeRotTime > 0.0:
rotationX = 0
rotationY = 0
rotationZ = 0
if self.animationRotationArray[self.rotationIndex][2] == 'catmullrom':
rotationX = getCatmullromLerpFromKeyframe(self.animationRotationArray, self.rotationIndex,
startToNowTime, 0)
rotationY = getCatmullromLerpFromKeyframe(self.animationRotationArray, self.rotationIndex,
startToNowTime, 1)
rotationZ = getCatmullromLerpFromKeyframe(self.animationRotationArray, self.rotationIndex,
startToNowTime, 2)
else:
rotationX = ((float(self.animationRotationArray[self.rotationIndex][1][0]) -
self.preAnimationRotArgs[1][0]) / keyframeRotTime * preToNowTime) + \
self.preAnimationRotArgs[1][0]
rotationY = ((float(self.animationRotationArray[self.rotationIndex][1][1]) -
self.preAnimationRotArgs[1][1]) / keyframeRotTime * preToNowTime) + \
self.preAnimationRotArgs[1][1]
rotationZ = ((float(self.animationRotationArray[self.rotationIndex][1][2]) -
self.preAnimationRotArgs[1][2]) / keyframeRotTime * preToNowTime) + \
self.preAnimationRotArgs[1][2]
deltX = self.rotationOrigin[0] if self.playConfig.isFirstPerson else 0
deltY = self.rotationOrigin[1] - (180 if self.playConfig.isFirstPerson else 0)
if self.playConfig.isAllowDrag:
nowCameraRotation = self.cameraCompFactory.GetCameraRotation()
deltX = (nowCameraRotation[0] - self.preAnimationRotArgs[1][0])
deltY = (nowCameraRotation[1] - self.preAnimationRotArgs[1][1])
deltX += rotationX
deltY += rotationY
rot = (
deltX % (-360 if deltX < 0 else 360),
deltY % (-360 if deltY < 0 else 360),
# 军械库版本,因为摄像机动画直接使用 Java 版动画,所以 y 轴没有旋转 180 度,因此 z 轴旋转从 - 改为 +,以匹配 Blockbench 里的效果
self.rotationOrigin[2] + rotationZ
)
# 是否不设置
isNotSet = False
if callable(self.playConfig.tickRotFunc):
isNotSet = self.playConfig.tickRotFunc(startToNowTime, rot)
if not isNotSet:
self.cameraCompFactory.SetCameraRotation(rot)
self.beforeRot = rot
self.preAnimationRotArgs = (
startToNowTime, [rotationX, rotationY, rotationZ]) # 更新上一帧的参数为当前参数,为下一次计算做准备
def HealthChangeClientEvent(self, event):
"""
生物生命值发生变化时触发
"""
# 自己死亡时,停止当前摄像机动画
playerType = clientApi.GetMinecraftEnum().EntityType.Player
if event['entityId'] == clientApi.GetLocalPlayerId() and compFactory.CreateEngineType(
event['entityId']).GetEngineType() & playerType == playerType and event['to'] <= 0.0:
self.stop(True)
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
import time
import mod.client.extraClientApi as clientApi
from .Camera import Camera, compFactory
# noinspection PyMethodMayBeStatic
class CameraListen(object):
"""
摄像机监听类,创建此类的对象,以手动控制摄像机的监听与取消监听
"""
cameraListenObj = None # type: CameraListen
def __init__(self):
pass
def GameRenderTickEvent(self, event):
if Camera.globalCameraObj:
Camera.globalCameraObj.GameRenderTickEvent(event)
def HealthChangeClientEvent(self, event):
if Camera.globalCameraObj:
Camera.globalCameraObj.HealthChangeClientEvent(event)
# 单个实例
CameraListen.cameraListenObj = CameraListen()
# 取消监听定时器
_timer = None
# 是否开启了内置监听
_enable = False
def startListen(client):
"""
开始监听
"""
global _enable
cameraListenObj = CameraListen.cameraListenObj
# 销毁自身已有监听
stopListen(client)
# 销毁摄像机对象内置监听
if Camera.globalCameraObj:
Camera.globalCameraObj.destroyListen()
_enable = True
client.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "GameRenderTickEvent", cameraListenObj, cameraListenObj.GameRenderTickEvent, 10)
client.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "HealthChangeClientEvent", cameraListenObj, cameraListenObj.HealthChangeClientEvent)
def stopListen(client, delayTime=0):
"""
取消监听
"""
global _timer
cameraListenObj = CameraListen.cameraListenObj
comp = compFactory.CreateGame(clientApi.GetLevelId())
def _scheduleCancel():
global _timer, _enable
# 如果取消监听时,发现还存在未停止的摄像机动画,不取消监听,再次创建一个定时器
if Camera.globalCameraObj and delayTime > 0:
if _timer:
_timer = comp.CancelTimer(_timer)
_timer = comp.AddTimer(delayTime, _scheduleCancel)
return
_enable = False
client.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "GameRenderTickEvent", cameraListenObj, cameraListenObj.GameRenderTickEvent)
client.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "HealthChangeClientEvent", cameraListenObj,
cameraListenObj.HealthChangeClientEvent)
if delayTime > 0:
_timer = comp.AddTimer(delayTime, _scheduleCancel)
else:
if _timer:
_timer = comp.CancelTimer(_timer)
_scheduleCancel()
def stopListenKeepLastAnim(client):
"""
取消监听,并且自动保留取消监听前最后一个摄像机动画的完整播放
"""
if Camera.globalCameraObj and Camera.globalCameraObj.animationLength is not None and Camera.globalCameraObj.startAnimationTime is not None:
t = Camera.globalCameraObj.animationLength - (time.time() - Camera.globalCameraObj.startAnimationTime)
stopListen(client, t)
else:
stopListen(client)
@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
import math
import time
import mod.client.extraClientApi as clientApi
from .utils.CatmullRom import getCatmullromLerpFromKeyframe
from .Camera import Camera, levelId, compFactory
from .exception.NotClientSystemException import NotClientSystemException
class Interpolation(object):
# 全局对象
globalObj = None
def __init__(self, client, keyframes, isReturn=False):
if not isinstance(client, clientApi.GetClientSystemCls()):
raise NotClientSystemException("传入 Interpolation 内的 client 不是一个 ClientSystem")
self.client = client
# 开始时间
self.startTime = time.time()
# 更新时间(再次调用 setInterpolation 时的时间)
self.updateTime = self.startTime
# 关键帧数组
self.keyframes = keyframes
# 关键帧数组索引
self.index = 0
# 前一帧索引,用于关键帧循环
self.preIndex = 0
# 前一帧计算出的旋转插值
self.preRot = (0, 0)
# 用于回正插值的缓存
self.tempRot = [0, 0, 0]
# 当前是否是回正插值
self.isReturn = isReturn
# 前一帧的摄像机角度
self.beforeRot = None
# 是否取消回正插值
self.cancelReturn = False
# 摄像机接口对象
self.cameraComponentFactory = compFactory.CreateCamera(levelId)
self.client.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "GameRenderTickEvent", self, self.onGameRenderTick, 0)
def onGameRenderTick(self, event):
"""
每 Tick 触发
"""
nowTime = time.time()
# 回正超时后自动停止
if self.isReturn and nowTime - self.startTime > self.keyframes[-1][0]:
self.stop()
# 当发现关键帧循环时,清空特定变量
if self.index < self.preIndex:
self.preRot = (0, 0)
startToNowTime = min(
nowTime - self.updateTime + self.keyframes[min(max(self.index - 1, 0), len(self.keyframes) - 1)][0],
self.keyframes[self.index][0]
)
rotX = getCatmullromLerpFromKeyframe(self.keyframes, self.index, startToNowTime, 0)
rotY = getCatmullromLerpFromKeyframe(self.keyframes, self.index, startToNowTime, 1)
nowRot = self.cameraComponentFactory.GetCameraRotation()
# 比较摄像机前后帧是否有较大的偏差,如果有,认为是玩家拖动屏幕造成的,取消回正
if self.beforeRot:
tolerance = 0.3
# y 轴角度是 -180 到 180 的循环,所以在计算角度差时要特殊处理
y = abs(nowRot[1] - self.beforeRot[1])
if abs(nowRot[0] - self.beforeRot[0]) > tolerance or min(y, 360 - y) > tolerance:
self.cancelReturn = True
# 混合之前的事件中设置的摄像机旋转
beforeEventRot = (0, 0, 0)
if Camera.globalCameraObj and Camera.globalCameraObj.beforeRot:
beforeEventRot = Camera.globalCameraObj.beforeRot
tempRot = (
rotX - self.preRot[0],
rotY - self.preRot[1],
0
)
deltX = nowRot[0] + tempRot[0]
deltY = nowRot[1] + tempRot[1]
rot = (
deltX % (-360 if deltX < 0 else 360),
deltY % (-360 if deltY < 0 else 360),
tempRot[2] + beforeEventRot[2]
)
self.cameraComponentFactory.SetCameraRotation(rot)
self.beforeRot = rot
self.preRot = (rotX, rotY)
self.preIndex = self.index
self.tempRot[0] -= tempRot[0]
self.tempRot[1] -= tempRot[1]
self.tempRot[2] -= tempRot[2]
def stop(self):
self.client.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "GameRenderTickEvent", self, self.onGameRenderTick)
Interpolation.globalObj = None
if not self.isReturn and not self.cancelReturn:
# 启动回正
endTime = math.sqrt(self.tempRot[0] * self.tempRot[0] + self.tempRot[1] * self.tempRot[1] + self.tempRot[2] * self.tempRot[2])
endTime *= 0.3
keyframes = [
(0.0, [0, 0, 0]),
(endTime, self.tempRot)
]
setInterpolation(self.client, keyframes, 1, isReturn=True)
def setInterpolation(client, keyframes, index, speed=10.0, isReturn=False):
"""
设置插值
Args:
client (ClientSystem): 客户端对象
keyframes (list): 关键帧数组,格式:[(0.0, [1.0, 2.5, 3.3]),(1.0, [-3.0, 2.5, 3.3])]
index (int): 关键帧数组索引
speed (float, optional): 速度。默认值 10.0
isReturn (bool, optional): 是否是回正插值。默认值 False
"""
if not Interpolation.globalObj:
keyframes = [(t / speed, i) for t, i in keyframes]
Interpolation.globalObj = Interpolation(client, keyframes, isReturn)
elif Interpolation.globalObj.isReturn:
# 正在回正中,强制停止
Interpolation.globalObj.stop()
keyframes = [(t / speed, i) for t, i in keyframes]
Interpolation.globalObj = Interpolation(client, keyframes, isReturn)
Interpolation.globalObj.updateTime = time.time()
Interpolation.globalObj.index = index
def stopInterpolation():
"""
停止插值
"""
if Interpolation.globalObj:
Interpolation.globalObj.stop()
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
class PlayConfig(object):
def __init__(self, isFirstPerson = False, isAllowDrag = False, isAllowMove = False, isPosFollowXRot = True, isPosFollowYRot = True, isZRotForceReturn = True, isZRotStartOrigin = False, isDelayOneFrame = True, isLockPerspective = True, tickPosFunc = None, tickRotFunc = None, playAfterFunc = None):
"""
play 方法的参数对象
Args:
isFirstPerson (bool, optional): 是否是第一人称动画。如果是,那么 Y 轴旋转会减 180 度。默认值 False
isAllowDrag (bool, optional): 是否允许玩家拖拽视角。默认值 False
isAllowMove (bool, optional): 是否允许玩家移动。默认值 False
isPosFollowXRot (bool, optional): 摄像机空间位置坐标是否受 X 轴旋转影响(仅在第一人称下生效)。默认值 True
isPosFollowYRot (bool, optional): 摄像机空间位置坐标是否受 Y 轴旋转影响(仅在第一人称下生效)。默认值 True
isZRotForceReturn (bool, optional): 是否在动画播放完成后强制归零 Z 轴的旋转值。默认值 True
isZRotStartOrigin (bool, optional): 是否将动画播放前的 Z 轴旋转值作为初始的 Z 轴旋转值。置为 False 可防止玩家在受伤瞬间播放摄像机动画造成的 Z 轴旋转异常(json 配置 override_previous_animation 值为 True 时无效)。默认值 False
isDelayOneFrame (bool, optional): 是否延迟一帧再播放摄像机动画。默认值 True
isLockPerspective (bool, optional): 是否锁定并强制切换玩家视角。默认值 True
tickPosFunc (function, optional): 摄像机动画播放时,在每次设置摄像机位置前执行的函数。默认值 None
tickRotFunc (function, optional): 摄像机动画播放时,在每次设置摄像机旋转前执行的函数。默认值 None
playAfterFunc (function, optional): 摄像机动画播放结束后的回调函数。默认值 None
"""
self.isFirstPerson = isFirstPerson
self.isAllowDrag = isAllowDrag
self.isAllowMove = isAllowMove
self.isPosFollowXRot = isPosFollowXRot
self.isPosFollowYRot = isPosFollowYRot
self.isZRotForceReturn = isZRotForceReturn
self.isZRotStartOrigin = isZRotStartOrigin
self.isDelayOneFrame = isDelayOneFrame
self.isLockPerspective = isLockPerspective
self.tickPosFunc = tickPosFunc
self.tickRotFunc = tickRotFunc
self.playAfterFunc = playAfterFunc
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
class NotClientSystemException(Exception):
"""
不是客户端系统异常
"""
def __init__(self, errorMessage):
super(NotClientSystemException, self).__init__(errorMessage)
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
class NotPlayConfigException(Exception):
"""
不是播放配置对象异常
"""
def __init__(self, errorMessage):
super(NotPlayConfigException, self).__init__(errorMessage)
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
class SafeEvalException(Exception):
"""
安全计算异常
"""
def __init__(self, errorMessage):
super(SafeEvalException, self).__init__(errorMessage)
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
class SplineCurve:
"""
Catmull-Rom 样条曲线类
"""
def __init__(self, points):
"""
初始化样条曲线
points: 值列表
"""
self.points = points
def getPoint(self, t):
"""根据参数 t 获取曲线上的点"""
if len(self.points) < 2:
return 0
# 处理边界情况
if t <= 0:
return self.points[0]
if t >= 1:
return self.points[-1]
# 找到包含 t 的线段
numSegments = len(self.points) - 1
segmentIndex = int(t * numSegments)
segmentIndex = max(0, min(segmentIndex, numSegments - 1))
# 计算线段内的局部参数
localT = t * numSegments - segmentIndex
# 获取四个控制点用于 Catmull-Rom 插值
p0Index = max(0, segmentIndex - 1)
p1Index = segmentIndex
p2Index = segmentIndex + 1
p3Index = min(len(self.points) - 1, segmentIndex + 2)
p0 = self.points[p0Index]
p1 = self.points[p1Index]
p2 = self.points[p2Index]
p3 = self.points[p3Index]
# Catmull-Rom 插值公式
t2 = localT * localT
t3 = t2 * localT
# 计算插值点的 y 坐标
y = 0.5 * (
(2 * p1) +
(-p0 + p2) * localT +
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +
(-p0 + 3 * p1 - 3 * p2 + p3) * t3
)
return y
def getCatmullromLerp(beforePlus, before, after, afterPlus, axis, alpha):
"""
Catmull-Rom 插值函数
Args:
beforePlus: 前一个关键帧的前一个关键帧
before: 前一个关键帧
after: 后一个关键帧
afterPlus: 后一个关键帧的后一个关键帧
axis: 坐标轴 (x: 0, y: 1, z: 2)
alpha: 插值参数 (0-1)
Returns:
插值结果
"""
vectors = []
# 构建控制点向量
if beforePlus:
vectors.append(beforePlus[1][axis])
if before:
vectors.append(before[1][axis])
if after:
vectors.append(after[1][axis])
if afterPlus:
vectors.append(afterPlus[1][axis])
# 创建样条曲线
curve = SplineCurve(vectors)
# 计算时间参数
timeParam = (alpha + (1 if beforePlus else 0)) / (len(vectors) - 1) if len(vectors) > 1 else 0
# 获取插值点并返回 y 坐标
point = curve.getPoint(timeParam)
return point
def getCatmullromLerpFromKeyframe(keyframeArray, index, nowTime, axis):
"""
通过关键帧数组获取当前时间点的平滑插值
"""
if not keyframeArray:
return 0
endIndex = len(keyframeArray) - 1
kf0 = keyframeArray[max(index - 2, 0)]
kf1 = keyframeArray[max(index - 1, 0)]
kf2 = keyframeArray[min(index, endIndex)]
kf3 = keyframeArray[min(index + 1, endIndex)]
den = (kf2[0] - kf1[0])
alpha = (nowTime - kf1[0]) / den if den != 0 else 0
return getCatmullromLerp(kf0, kf1, kf2, kf3, axis, alpha)
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
from ..exception.SafeEvalException import SafeEvalException
class SafeEval:
def __init__(self):
self.operators = {
'+': lambda x, y: x + y,
'-': lambda x, y: x - y,
'*': lambda x, y: x * y,
'/': lambda x, y: x / y if y != 0 else self.raiseException("发生了除以 0 错误")
}
def tokenize(self, expression):
"""将表达式分割成数字和运算符"""
tokens = []
number = ''
negative = False # 用于标记负号
for char in expression:
if char.isspace():
continue
elif char.isdigit() or char == '.':
number += char
elif char == '-':
if not number: # 如果前面没有数字,则表示负号
negative = True
else:
if number:
tokens.append(float(number) if not negative else -float(number))
number = ''
negative = False
tokens.append(char)
else:
if number:
tokens.append(float(number) if not negative else -float(number))
number = ''
negative = False
tokens.append(char)
if number:
tokens.append(float(number) if not negative else -float(number))
return tokens
def evaluate(self, expression):
"""计算表达式的值"""
tokens = self.tokenize(expression)
return self.parse_expression(tokens)
def parse_expression(self, tokens):
"""解析表达式"""
return self.parse_additive(tokens)
def parse_additive(self, tokens):
"""解析加法和减法"""
result = self.parse_multiplicative(tokens)
while tokens and tokens[0] in ('+', '-'):
op = tokens.pop(0)
if op == '+':
right = self.parse_multiplicative(tokens)
result = self.operators[op](result, right)
elif op == '-':
right = self.parse_multiplicative(tokens)
result = self.operators[op](result, right)
return result
def parse_multiplicative(self, tokens):
"""解析乘法和除法"""
result = self.parse_primary(tokens)
while tokens and tokens[0] in ('*', '/'):
op = tokens.pop(0)
if op == '*':
right = self.parse_primary(tokens)
result = self.operators[op](result, right)
elif op == '/':
right = self.parse_primary(tokens)
result = self.operators[op](result, right)
return result
def parse_primary(self, tokens):
"""解析基本表达式"""
if not tokens:
return None
token = tokens.pop(0)
if isinstance(token, float):
return token
elif token == '(':
result = self.parse_expression(tokens)
if not tokens or tokens[0] != ')':
return self.raiseException("括号不匹配")
tokens.pop(0) # Pop the closing parenthesis
return result
else:
return self.raiseException("无效的语法")
def raiseException(self, message):
"""抛出异常"""
SafeEvalException(message)