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 Slow down strafe movement somehow
Better creature damage sfx Better creature damage sfx
Audio debugging Audio debugging
Play animations offset by network delay
- Attack animation
# TODO # TODO

View File

@ -173,6 +173,20 @@ public class AudioEngine {
} }
} }
/**
* 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 * 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 * Default audio files to play. Eventually should probably refactor into service that determines audio based on materials
*/ */
static String[] defaultHitboxAudio = new String[]{ 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 B.wav",
// "Audio/weapons/collisions/Massive Punch C.wav", // "Audio/weapons/collisions/Massive Punch C.wav",
}; };

View File

@ -475,7 +475,7 @@ public class Globals {
"/Audio/weapons/swordUnsheath1.ogg", "/Audio/weapons/swordUnsheath1.ogg",
"/Audio/weapons/swoosh-03.ogg", "/Audio/weapons/swoosh-03.ogg",
"/Audio/movement/Equip A.wav", "/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 A.wav",
"/Audio/weapons/collisions/Massive Punch B.wav", "/Audio/weapons/collisions/Massive Punch B.wav",
"/Audio/weapons/collisions/Massive Punch C.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 //tracks if this is the server or not
boolean isServer; boolean isServer;
//If set to true on client, will account for delay between client and server when starting animations
boolean accountForSync = false;
/** /**
* Private constructor * Private constructor
* @param states * @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 * Simulates a given state
* @param stateEnum The enum for the state * @param stateEnum The enum for the state
@ -98,7 +109,8 @@ public class StateTransitionUtil {
* @param parent The parent entity * @param parent The parent entity
* @param state The state * @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); Actor actor = EntityUtils.getActor(parent);
if(actor != null){ if(actor != null){
@ -113,6 +125,15 @@ public class StateTransitionUtil {
audioData = state.getAudio.get(); 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 //Play main animation
@ -122,7 +143,14 @@ public class StateTransitionUtil {
state.onComplete.run(); state.onComplete.run();
state.startedAnimation = false; state.startedAnimation = false;
} else if(!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animation)){ } else if(!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animation)){
//play animation, audio, etc, for state
//
//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){ if(parent == Globals.playerEntity && !Globals.controlHandler.cameraIsThirdPerson() && animation != null){
//first person //first person
//play first person audio //play first person audio
@ -136,10 +164,14 @@ public class StateTransitionUtil {
} }
} }
//
//play animation
if(animation != null){ if(animation != null){
actor.playAnimation(animation,true); actor.playAnimation(animation,true);
shouldPlayFirstPerson = true;
}
actor.incrementAnimationTime(animationOffset);
} }
actor.incrementAnimationTime(0.0001);
state.startedAnimation = true; state.startedAnimation = true;
} else if(state.animation == null && state.onComplete != null){ } else if(state.animation == null && state.onComplete != null){
state.onComplete.run(); state.onComplete.run();
@ -149,8 +181,8 @@ public class StateTransitionUtil {
// //
//Play animation in first person //Play animation in first person
// //
if(animation != null){ if(shouldPlayFirstPerson && animation != null){
FirstPersonTree.conditionallyPlayAnimation(parent, animation); FirstPersonTree.conditionallyPlayAnimation(parent, animation, animationOffset);
} }
} }
} }
@ -160,7 +192,7 @@ public class StateTransitionUtil {
* @param parent The parent entity * @param parent The parent entity
* @param state The state * @param state The state
*/ */
private static void simulateServerState(Entity parent, StateTransitionUtilItem state){ private void simulateServerState(Entity parent, StateTransitionUtilItem state){
PoseActor poseActor = EntityUtils.getPoseActor(parent); PoseActor poseActor = EntityUtils.getPoseActor(parent);
if(poseActor != null){ if(poseActor != null){
@ -185,11 +217,14 @@ public class StateTransitionUtil {
state.onComplete.run(); state.onComplete.run();
state.startedAnimation = false; state.startedAnimation = false;
} else if(!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animation)){ } else if(!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animation)){
//if it isn't looping, only play on first go around
if(state.loop || !state.startedAnimation){
//play animation for state //play animation for state
if(animation != null){ if(animation != null){
poseActor.playAnimation(animation); poseActor.playAnimation(animation);
} }
poseActor.incrementAnimationTime(0.0001); poseActor.incrementAnimationTime(0.0001);
}
state.startedAnimation = true; state.startedAnimation = true;
} }
} }
@ -299,6 +334,9 @@ public class StateTransitionUtil {
//The function to fire on completion (ie to transition to the next state) //The function to fire on completion (ie to transition to the next state)
Runnable onComplete; Runnable onComplete;
//Controls whether the state transition util should loop or not
boolean loop = true;
//Tracks whether the animation has been played or not //Tracks whether the animation has been played or not
boolean startedAnimation = false; boolean startedAnimation = false;
@ -315,6 +353,24 @@ public class StateTransitionUtil {
this.animation = animation; this.animation = animation;
this.audioData = audioData; this.audioData = audioData;
this.onComplete = onComplete; 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.getAnimation = getAnimation;
this.getAudio = getAudio; this.getAudio = getAudio;
this.onComplete = onComplete; 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 * 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 stateEnum The enum value for this state in particular in the tree
@ -385,6 +483,37 @@ public class StateTransitionUtil {
return rVal; 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 //the current state of the tree
@SyncedField @SyncedField(serverSendTransitionPacket = true)
AttackTreeState state; AttackTreeState state;
//the current state of drifting caused by the tree //the current state of drifting caused by the tree
@ -126,7 +126,7 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData(); return state.getAudioData();
} }
}, },
null false
), ),
StateTransitionUtilItem.create( StateTransitionUtilItem.create(
AttackTreeState.HOLD, AttackTreeState.HOLD,
@ -152,7 +152,7 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData(); return state.getAudioData();
} }
}, },
null true
), ),
StateTransitionUtilItem.create( StateTransitionUtilItem.create(
AttackTreeState.ATTACK, AttackTreeState.ATTACK,
@ -178,7 +178,7 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData(); return state.getAudioData();
} }
}, },
null false
), ),
StateTransitionUtilItem.create( StateTransitionUtilItem.create(
AttackTreeState.COOLDOWN, AttackTreeState.COOLDOWN,
@ -204,9 +204,10 @@ public class ClientAttackTree implements BehaviorTree {
return state.getAudioData(); return state.getAudioData();
} }
}, },
null false
), ),
}); });
this.stateTransitionUtil.setAccountForSync(true);
} }
/** /**
@ -668,4 +669,17 @@ public class ClientAttackTree implements BehaviorTree {
public void setCurrentMoveId(String currentMoveId){ public void setCurrentMoveId(String currentMoveId){
this.currentMoveId = 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 { public class ServerAttackTree implements BehaviorTree {
@SyncedField @SyncedField(serverSendTransitionPacket = true)
//the state of the attack tree //the state of the attack tree
AttackTreeState state; AttackTreeState state;
@ -220,6 +220,7 @@ public class ServerAttackTree implements BehaviorTree {
} else { } else {
LoggerInterface.loggerEngine.ERROR(new IllegalStateException("Trying to start attacking tree, but current move does not have windup or attack states defined!")); 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 //intuit can hold from presence of windup anim
currentMoveCanHold = currentMove.getHoldState() != null; currentMoveCanHold = currentMove.getHoldState() != null;
//clear collided list //clear collided list
@ -601,7 +602,7 @@ public class ServerAttackTree implements BehaviorTree {
public void setState(AttackTreeState state){ public void setState(AttackTreeState state){
this.state = state; this.state = state;
int value = ClientAttackTree.getAttackTreeStateEnumAsShort(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> * <p> Automatically generated </p>

View File

@ -93,6 +93,16 @@ public class FirstPersonTree implements BehaviorTree {
* @param animationName the name of the animation * @param animationName the name of the animation
*/ */
public void playAnimation(String animationName, int priority){ 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){ if(Globals.firstPersonEntity != null){
Actor actor = EntityUtils.getActor(Globals.firstPersonEntity); Actor actor = EntityUtils.getActor(Globals.firstPersonEntity);
if( if(
@ -100,7 +110,7 @@ public class FirstPersonTree implements BehaviorTree {
(Globals.assetManager.fetchModel(actor.getModelPath()) != null && Globals.assetManager.fetchModel(actor.getModelPath()).getAnimation(animationName) != null) (Globals.assetManager.fetchModel(actor.getModelPath()) != null && Globals.assetManager.fetchModel(actor.getModelPath()).getAnimation(animationName) != null)
){ ){
actor.playAnimation(animationName,priority); 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 * @param animationName the name of the animation
*/ */
public void playAnimation(TreeDataAnimation 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){ if(Globals.firstPersonEntity != null){
Actor actor = EntityUtils.getActor(Globals.firstPersonEntity); Actor actor = EntityUtils.getActor(Globals.firstPersonEntity);
if( 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) (Globals.assetManager.fetchModel(actor.getModelPath()) != null && Globals.assetManager.fetchModel(actor.getModelPath()).getAnimation(animation.getNameFirstPerson()) != null)
){ ){
actor.playAnimation(animation, false); 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 animationName the name of the animation
* @param priority The priority 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)){ if(entity != null && entity == Globals.playerEntity && FirstPersonTree.hasTree(Globals.firstPersonEntity)){
FirstPersonTree.getTree(Globals.firstPersonEntity).playAnimation(animationName, priority); 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 * If the entity has a first person tree, plays the provided animation
* @param entity The entity * @param entity The entity
@ -161,6 +190,18 @@ public class FirstPersonTree implements BehaviorTree {
} }
} }
/**
* 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 * If the entity has a first person tree, interrupts the provided animation
* @param entity The entity * @param entity The entity

View File

@ -42,6 +42,11 @@ public class ImGuiAudio {
ImGui.text("Listener location: " + Globals.audioEngine.getListener().getPosition()); ImGui.text("Listener location: " + Globals.audioEngine.getListener().getPosition());
ImGui.text("Listener eye vector: " + Globals.audioEngine.getListener().getEyeVector()); ImGui.text("Listener eye vector: " + Globals.audioEngine.getListener().getEyeVector());
ImGui.text("Listener up vector: " + Globals.audioEngine.getListener().getUpVector()); 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 //only active children
if(ImGui.collapsingHeader("Mapped Virtual Sources")){ 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){ private void transitionBTree(Entity entity, int bTreeId, SynchronizationMessage message){
switch(bTreeId){ 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: { case BehaviorTreeIdEnums.BTREE_SERVERBLOCKTREE_ID: {
switch(message.getfieldId()){ switch(message.getfieldId()){
case FieldIdEnums.TREE_SERVERBLOCKTREE_SYNCEDFIELD_STATE_ID:{ case FieldIdEnums.TREE_SERVERBLOCKTREE_SYNCEDFIELD_STATE_ID:{