client animation offset by network delay
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2024-08-15 16:46:10 -04:00
parent e9257784c6
commit d15ba2445a
12 changed files with 258 additions and 36 deletions

Binary file not shown.

View File

@ -580,6 +580,8 @@ Walk tree
Slow down strafe movement somehow
Better creature damage sfx
Audio debugging
Play animations offset by network delay
- Attack animation
# TODO

View File

@ -172,6 +172,20 @@ public class AudioEngine {
LoggerInterface.loggerAudio.INFO(audioType.getExtension() + " support: " + AudioSystem.isFileTypeSupported(audioType));
}
}
/**
* Gets the support formats
* @return The list of file extensions
*/
public List<String> getSupportedFormats(){
List<String> rVal = new LinkedList<String>();
for(AudioFileFormat.Type audioType : AudioSystem.getAudioFileTypes()){
if(AudioSystem.isFileTypeSupported(audioType)){
rVal.add(audioType.getExtension());
}
}
return rVal;
}
/**
* Updates the orientation of the listener based on the global player camera

View File

@ -22,7 +22,7 @@ public class HitboxAudioService {
* Default audio files to play. Eventually should probably refactor into service that determines audio based on materials
*/
static String[] defaultHitboxAudio = new String[]{
"Audio/weapons/collisions/FleshWeaponHit1.ogg",
"Audio/weapons/collisions/FleshWeaponHit1.wav",
// "Audio/weapons/collisions/Massive Punch B.wav",
// "Audio/weapons/collisions/Massive Punch C.wav",
};

View File

@ -475,7 +475,7 @@ public class Globals {
"/Audio/weapons/swordUnsheath1.ogg",
"/Audio/weapons/swoosh-03.ogg",
"/Audio/movement/Equip A.wav",
"/Audio/weapons/collisions/FleshWeaponHit1.ogg",
"/Audio/weapons/collisions/FleshWeaponHit1.wav",
"/Audio/weapons/collisions/Massive Punch A.wav",
"/Audio/weapons/collisions/Massive Punch B.wav",
"/Audio/weapons/collisions/Massive Punch C.wav",

View File

@ -31,6 +31,9 @@ public class StateTransitionUtil {
//tracks if this is the server or not
boolean isServer;
//If set to true on client, will account for delay between client and server when starting animations
boolean accountForSync = false;
/**
* Private constructor
* @param states
@ -70,6 +73,14 @@ public class StateTransitionUtil {
}
}
/**
* Sets whether the client will account for delay over network or not
* @param accountForSync true if account for delay, false otherwise
*/
public void setAccountForSync(boolean accountForSync){
this.accountForSync = accountForSync;
}
/**
* Simulates a given state
* @param stateEnum The enum for the state
@ -98,7 +109,8 @@ public class StateTransitionUtil {
* @param parent The parent entity
* @param state The state
*/
private static void simulateClientState(Entity parent, StateTransitionUtilItem state){
private void simulateClientState(Entity parent, StateTransitionUtilItem state){
boolean shouldPlayFirstPerson = false;
Actor actor = EntityUtils.getActor(parent);
if(actor != null){
@ -113,6 +125,15 @@ public class StateTransitionUtil {
audioData = state.getAudio.get();
}
//
//Calculate offset to start the animation at
double animationOffset = 0.0001;
if(this.accountForSync){
int delay = Globals.clientConnection.getDelay();
double simFrameTime = Globals.timekeeper.getSimFrameTime();
animationOffset = delay * simFrameTime;
}
//
//Play main animation
@ -122,24 +143,35 @@ public class StateTransitionUtil {
state.onComplete.run();
state.startedAnimation = false;
} else if(!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animation)){
//play animation, audio, etc, for state
if(parent == Globals.playerEntity && !Globals.controlHandler.cameraIsThirdPerson() && animation != null){
//first person
//play first person audio
if(Globals.audioEngine.initialized() && audioData != null && audioData.getAudioPath() != null){
Globals.virtualAudioSourceManager.createVirtualAudioSource(audioData.getAudioPath(), VirtualAudioSourceType.CREATURE, false);
}
} else {
//play third person audio
if(Globals.audioEngine.initialized() && audioData != null && audioData.getAudioPath() != null){
Globals.virtualAudioSourceManager.createVirtualAudioSource(audioData.getAudioPath(), VirtualAudioSourceType.CREATURE, false, EntityUtils.getPosition(parent));
}
}
if(animation != null){
actor.playAnimation(animation,true);
//
//if it isn't looping, only play on first go around
if(state.loop || !state.startedAnimation){
//
//play audio
if(parent == Globals.playerEntity && !Globals.controlHandler.cameraIsThirdPerson() && animation != null){
//first person
//play first person audio
if(Globals.audioEngine.initialized() && audioData != null && audioData.getAudioPath() != null){
Globals.virtualAudioSourceManager.createVirtualAudioSource(audioData.getAudioPath(), VirtualAudioSourceType.CREATURE, false);
}
} else {
//play third person audio
if(Globals.audioEngine.initialized() && audioData != null && audioData.getAudioPath() != null){
Globals.virtualAudioSourceManager.createVirtualAudioSource(audioData.getAudioPath(), VirtualAudioSourceType.CREATURE, false, EntityUtils.getPosition(parent));
}
}
//
//play animation
if(animation != null){
actor.playAnimation(animation,true);
shouldPlayFirstPerson = true;
}
actor.incrementAnimationTime(animationOffset);
}
actor.incrementAnimationTime(0.0001);
state.startedAnimation = true;
} else if(state.animation == null && state.onComplete != null){
state.onComplete.run();
@ -149,8 +181,8 @@ public class StateTransitionUtil {
//
//Play animation in first person
//
if(animation != null){
FirstPersonTree.conditionallyPlayAnimation(parent, animation);
if(shouldPlayFirstPerson && animation != null){
FirstPersonTree.conditionallyPlayAnimation(parent, animation, animationOffset);
}
}
}
@ -160,7 +192,7 @@ public class StateTransitionUtil {
* @param parent The parent entity
* @param state The state
*/
private static void simulateServerState(Entity parent, StateTransitionUtilItem state){
private void simulateServerState(Entity parent, StateTransitionUtilItem state){
PoseActor poseActor = EntityUtils.getPoseActor(parent);
if(poseActor != null){
@ -185,11 +217,14 @@ public class StateTransitionUtil {
state.onComplete.run();
state.startedAnimation = false;
} else if(!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animation)){
//play animation for state
if(animation != null){
poseActor.playAnimation(animation);
//if it isn't looping, only play on first go around
if(state.loop || !state.startedAnimation){
//play animation for state
if(animation != null){
poseActor.playAnimation(animation);
}
poseActor.incrementAnimationTime(0.0001);
}
poseActor.incrementAnimationTime(0.0001);
state.startedAnimation = true;
}
}
@ -299,6 +334,9 @@ public class StateTransitionUtil {
//The function to fire on completion (ie to transition to the next state)
Runnable onComplete;
//Controls whether the state transition util should loop or not
boolean loop = true;
//Tracks whether the animation has been played or not
boolean startedAnimation = false;
@ -315,6 +353,24 @@ public class StateTransitionUtil {
this.animation = animation;
this.audioData = audioData;
this.onComplete = onComplete;
if(this.onComplete != null){
this.loop = false;
}
}
/**
* Constructor
*/
private StateTransitionUtilItem(
Object stateEnum,
TreeDataAnimation animation,
TreeDataAudio audioData,
boolean loop
){
this.stateEnum = stateEnum;
this.animation = animation;
this.audioData = audioData;
this.loop = loop;
}
/**
@ -330,6 +386,24 @@ public class StateTransitionUtil {
this.getAnimation = getAnimation;
this.getAudio = getAudio;
this.onComplete = onComplete;
if(this.onComplete != null){
this.loop = false;
}
}
/**
* Constructor for supplier type
*/
private StateTransitionUtilItem(
Object stateEnum,
Supplier<TreeDataAnimation> getAnimation,
Supplier<TreeDataAudio> getAudio,
boolean loop
){
this.stateEnum = stateEnum;
this.getAnimation = getAnimation;
this.getAudio = getAudio;
this.loop = loop;
}
/**
@ -355,6 +429,30 @@ public class StateTransitionUtil {
);
}
/**
* Constructor for a supplier-based approach. This takes suppliers that will provide animation data on demand.
* This decouples the animations from the initialization of the tree.
* The intended usecase is if the animation could change based on some state in the tree.
* @param stateEnum The enum value for this state
* @param getAnimationData The supplier for the animation data. If it is null, it will not play any animation
* @param getAudio The supplier for path to an audio file to play on starting the animation. If null, no audio will be played
* @param loop Sets whether the animation should loop or not
* @return
*/
public static StateTransitionUtilItem create(
Object stateEnum,
Supplier<TreeDataAnimation> getAnimation,
Supplier<TreeDataAudio> getAudio,
boolean loop
){
return new StateTransitionUtilItem(
stateEnum,
getAnimation,
getAudio,
loop
);
}
/**
* Creates a state transition based on tree data for the state
* @param stateEnum The enum value for this state in particular in the tree
@ -385,6 +483,37 @@ public class StateTransitionUtil {
return rVal;
}
/**
* Creates a state transition based on tree data for the state
* @param stateEnum The enum value for this state in particular in the tree
* @param treeData The tree data for this state
* @param loop Controls whether the state loops its animation or not
* @return The item for the transition util
*/
public static StateTransitionUtilItem create(
Object stateEnum,
TreeDataState treeData,
boolean loop
){
StateTransitionUtilItem rVal = null;
if(treeData != null){
rVal = new StateTransitionUtilItem(
stateEnum,
treeData.getAnimation(),
treeData.getAudioData(),
loop
);
} else {
rVal = new StateTransitionUtilItem(
stateEnum,
(TreeDataAnimation)null,
null,
loop
);
}
return rVal;
}
}

View File

@ -57,7 +57,7 @@ public class ClientAttackTree implements BehaviorTree {
}
//the current state of the tree
@SyncedField
@SyncedField(serverSendTransitionPacket = true)
AttackTreeState state;
//the current state of drifting caused by the tree
@ -126,7 +126,7 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData();
}
},
null
false
),
StateTransitionUtilItem.create(
AttackTreeState.HOLD,
@ -152,7 +152,7 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData();
}
},
null
true
),
StateTransitionUtilItem.create(
AttackTreeState.ATTACK,
@ -178,7 +178,7 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData();
}
},
null
false
),
StateTransitionUtilItem.create(
AttackTreeState.COOLDOWN,
@ -204,9 +204,10 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData();
}
},
null
false
),
});
this.stateTransitionUtil.setAccountForSync(true);
}
/**
@ -668,4 +669,17 @@ public class ClientAttackTree implements BehaviorTree {
public void setCurrentMoveId(String currentMoveId){
this.currentMoveId = currentMoveId;
}
/**
* <p> (Initially) Automatically Generated </p>
* <p>
* Performs a state transition on a client state variable.
* Will be triggered when a server performs a state change.
* </p>
* @param newState The new value of the state
*/
public void transitionState(AttackTreeState newState){
this.stateTransitionUtil.reset();
this.setState(newState);
}
}

View File

@ -49,7 +49,7 @@ import org.joml.Vector3f;
*/
public class ServerAttackTree implements BehaviorTree {
@SyncedField
@SyncedField(serverSendTransitionPacket = true)
//the state of the attack tree
AttackTreeState state;
@ -220,6 +220,7 @@ public class ServerAttackTree implements BehaviorTree {
} else {
LoggerInterface.loggerEngine.ERROR(new IllegalStateException("Trying to start attacking tree, but current move does not have windup or attack states defined!"));
}
this.stateTransitionUtil.reset();
//intuit can hold from presence of windup anim
currentMoveCanHold = currentMove.getHoldState() != null;
//clear collided list
@ -601,7 +602,7 @@ public class ServerAttackTree implements BehaviorTree {
public void setState(AttackTreeState state){
this.state = state;
int value = ClientAttackTree.getAttackTreeStateEnumAsShort(state);
DataCellSearchUtils.getEntityDataCell(parent).broadcastNetworkMessage(SynchronizationMessage.constructUpdateClientStateMessage(parent.getId(), BehaviorTreeIdEnums.BTREE_SERVERATTACKTREE_ID, FieldIdEnums.TREE_SERVERATTACKTREE_SYNCEDFIELD_STATE_ID, value));
DataCellSearchUtils.getEntityDataCell(parent).broadcastNetworkMessage(SynchronizationMessage.constructServerNotifyBTreeTransitionMessage(parent.getId(), BehaviorTreeIdEnums.BTREE_SERVERATTACKTREE_ID, FieldIdEnums.TREE_SERVERATTACKTREE_SYNCEDFIELD_STATE_ID, value));
}
/**
* <p> Automatically generated </p>

View File

@ -93,6 +93,16 @@ public class FirstPersonTree implements BehaviorTree {
* @param animationName the name of the animation
*/
public void playAnimation(String animationName, int priority){
this.playAnimation(animationName, priority, 0.0001);
}
/**
* Plays an animation if it exists
* @param animationName the name of the animation
* @param priority The priority to play the animation with
* @param offset The offset to start the animation at
*/
public void playAnimation(String animationName, int priority, double offset){
if(Globals.firstPersonEntity != null){
Actor actor = EntityUtils.getActor(Globals.firstPersonEntity);
if(
@ -100,7 +110,7 @@ public class FirstPersonTree implements BehaviorTree {
(Globals.assetManager.fetchModel(actor.getModelPath()) != null && Globals.assetManager.fetchModel(actor.getModelPath()).getAnimation(animationName) != null)
){
actor.playAnimation(animationName,priority);
actor.incrementAnimationTime(0.0001);
actor.incrementAnimationTime(offset);
}
}
}
@ -110,6 +120,15 @@ public class FirstPersonTree implements BehaviorTree {
* @param animationName the name of the animation
*/
public void playAnimation(TreeDataAnimation animation){
this.playAnimation(animation, 0.0001);
}
/**
* Plays an animation if it exists
* @param animationName the name of the animation
* @param offset The offset to start the animation at
*/
public void playAnimation(TreeDataAnimation animation, double offset){
if(Globals.firstPersonEntity != null){
Actor actor = EntityUtils.getActor(Globals.firstPersonEntity);
if(
@ -117,7 +136,7 @@ public class FirstPersonTree implements BehaviorTree {
(Globals.assetManager.fetchModel(actor.getModelPath()) != null && Globals.assetManager.fetchModel(actor.getModelPath()).getAnimation(animation.getNameFirstPerson()) != null)
){
actor.playAnimation(animation, false);
actor.incrementAnimationTime(0.0001);
actor.incrementAnimationTime(offset);
}
}
}
@ -144,12 +163,22 @@ public class FirstPersonTree implements BehaviorTree {
* @param animationName the name of the animation
* @param priority The priority of the animation
*/
public static void conditionallyPlayAnimation(Entity entity, String animationName, int priority){
public static void conditionallyPlayAnimation(Entity entity, String animationName, int priority, double offset){
if(entity != null && entity == Globals.playerEntity && FirstPersonTree.hasTree(Globals.firstPersonEntity)){
FirstPersonTree.getTree(Globals.firstPersonEntity).playAnimation(animationName, priority);
}
}
/**
* If the entity has a first person tree, plays the provided animation
* @param entity The entity
* @param animationName the name of the animation
* @param priority The priority of the animation
*/
public static void conditionallyPlayAnimation(Entity entity, String animationName, int priority){
FirstPersonTree.conditionallyPlayAnimation(entity, animationName, priority, 0.0001);
}
/**
* If the entity has a first person tree, plays the provided animation
* @param entity The entity
@ -160,6 +189,18 @@ public class FirstPersonTree implements BehaviorTree {
FirstPersonTree.getTree(Globals.firstPersonEntity).playAnimation(animation);
}
}
/**
* If the entity has a first person tree, plays the provided animation
* @param entity The entity
* @param animationName the name of the animation
* @param offset The offset to start the animation at
*/
public static void conditionallyPlayAnimation(Entity entity, TreeDataAnimation animation, double offset){
if(entity != null && entity == Globals.playerEntity && FirstPersonTree.hasTree(Globals.firstPersonEntity)){
FirstPersonTree.getTree(Globals.firstPersonEntity).playAnimation(animation,offset);
}
}
/**
* If the entity has a first person tree, interrupts the provided animation

View File

@ -42,6 +42,11 @@ public class ImGuiAudio {
ImGui.text("Listener location: " + Globals.audioEngine.getListener().getPosition());
ImGui.text("Listener eye vector: " + Globals.audioEngine.getListener().getEyeVector());
ImGui.text("Listener up vector: " + Globals.audioEngine.getListener().getUpVector());
if(ImGui.collapsingHeader("Supported (Java-loaded) formats")){
for(String extension : Globals.audioEngine.getSupportedFormats()){
ImGui.text(extension);
}
}
}
//only active children
if(ImGui.collapsingHeader("Mapped Virtual Sources")){

View File

@ -204,6 +204,14 @@ public class ClientNetworking implements Runnable {
}
/**
* Gets the delay across the connection
* @return The delay
*/
public int getDelay(){
return (int)(lastPongTime - lastPingTime);
}
/**

View File

@ -245,6 +245,14 @@ public class ClientSynchronizationManager {
*/
private void transitionBTree(Entity entity, int bTreeId, SynchronizationMessage message){
switch(bTreeId){
case BehaviorTreeIdEnums.BTREE_SERVERATTACKTREE_ID: {
switch(message.getfieldId()){
case FieldIdEnums.TREE_SERVERATTACKTREE_SYNCEDFIELD_STATE_ID:{
ClientAttackTree tree = ClientAttackTree.getClientAttackTree(entity);
tree.transitionState(ClientAttackTree.getAttackTreeStateShortAsEnum((short)message.getbTreeValue()));
} break;
}
} break;
case BehaviorTreeIdEnums.BTREE_SERVERBLOCKTREE_ID: {
switch(message.getfieldId()){
case FieldIdEnums.TREE_SERVERBLOCKTREE_SYNCEDFIELD_STATE_ID:{