diff --git a/assets/Audio/weapons/collisions/FleshWeaponHit1.wav b/assets/Audio/weapons/collisions/FleshWeaponHit1.wav new file mode 100644 index 00000000..84415df8 Binary files /dev/null and b/assets/Audio/weapons/collisions/FleshWeaponHit1.wav differ diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 5ba3b077..0b207ae2 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -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 diff --git a/src/main/java/electrosphere/audio/AudioEngine.java b/src/main/java/electrosphere/audio/AudioEngine.java index ec0827f9..44d4dd2d 100644 --- a/src/main/java/electrosphere/audio/AudioEngine.java +++ b/src/main/java/electrosphere/audio/AudioEngine.java @@ -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 getSupportedFormats(){ + List rVal = new LinkedList(); + 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 diff --git a/src/main/java/electrosphere/audio/collision/HitboxAudioService.java b/src/main/java/electrosphere/audio/collision/HitboxAudioService.java index ea64fe97..df410636 100644 --- a/src/main/java/electrosphere/audio/collision/HitboxAudioService.java +++ b/src/main/java/electrosphere/audio/collision/HitboxAudioService.java @@ -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", }; diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index 16ad6a43..e21cee08 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -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", diff --git a/src/main/java/electrosphere/entity/btree/StateTransitionUtil.java b/src/main/java/electrosphere/entity/btree/StateTransitionUtil.java index dfb28d04..a6694505 100644 --- a/src/main/java/electrosphere/entity/btree/StateTransitionUtil.java +++ b/src/main/java/electrosphere/entity/btree/StateTransitionUtil.java @@ -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 getAnimation, + Supplier 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 getAnimation, + Supplier 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; + } + } diff --git a/src/main/java/electrosphere/entity/state/attack/ClientAttackTree.java b/src/main/java/electrosphere/entity/state/attack/ClientAttackTree.java index 41f35277..b389319f 100644 --- a/src/main/java/electrosphere/entity/state/attack/ClientAttackTree.java +++ b/src/main/java/electrosphere/entity/state/attack/ClientAttackTree.java @@ -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; } + /** + *

(Initially) Automatically Generated

+ *

+ * Performs a state transition on a client state variable. + * Will be triggered when a server performs a state change. + *

+ * @param newState The new value of the state + */ + public void transitionState(AttackTreeState newState){ + this.stateTransitionUtil.reset(); + this.setState(newState); + } + } diff --git a/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java b/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java index 515d1ab6..50b8e50d 100644 --- a/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java +++ b/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java @@ -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)); } /** *

Automatically generated

diff --git a/src/main/java/electrosphere/entity/state/client/firstPerson/FirstPersonTree.java b/src/main/java/electrosphere/entity/state/client/firstPerson/FirstPersonTree.java index 6808c315..a76167cc 100644 --- a/src/main/java/electrosphere/entity/state/client/firstPerson/FirstPersonTree.java +++ b/src/main/java/electrosphere/entity/state/client/firstPerson/FirstPersonTree.java @@ -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 diff --git a/src/main/java/electrosphere/menu/debug/ImGuiAudio.java b/src/main/java/electrosphere/menu/debug/ImGuiAudio.java index a58708a8..9fff72a0 100644 --- a/src/main/java/electrosphere/menu/debug/ImGuiAudio.java +++ b/src/main/java/electrosphere/menu/debug/ImGuiAudio.java @@ -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")){ diff --git a/src/main/java/electrosphere/net/client/ClientNetworking.java b/src/main/java/electrosphere/net/client/ClientNetworking.java index 1311c1bf..06ae9e59 100644 --- a/src/main/java/electrosphere/net/client/ClientNetworking.java +++ b/src/main/java/electrosphere/net/client/ClientNetworking.java @@ -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); + } + /** diff --git a/src/main/java/electrosphere/net/synchronization/client/ClientSynchronizationManager.java b/src/main/java/electrosphere/net/synchronization/client/ClientSynchronizationManager.java index 49b23f9e..68ef5315 100644 --- a/src/main/java/electrosphere/net/synchronization/client/ClientSynchronizationManager.java +++ b/src/main/java/electrosphere/net/synchronization/client/ClientSynchronizationManager.java @@ -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:{