simple animation state transition util
Some checks failed
studiorailgun/Renderer/pipeline/head There was a failure building this commit

This commit is contained in:
austin 2024-07-26 19:01:32 -04:00
parent 5f56f0d742
commit 6c3dcec01c
7 changed files with 450 additions and 62 deletions

View File

@ -290,9 +290,8 @@
"variants": [
{
"variantId": "blockWeaponRight",
"windUpAnimation" : "Jump",
"mainAnimation" : "Fall",
"cooldownAnimation" : "Land",
"mainFirstPersonAnimation" : "HoldItemR2HBlock",
"defaults" : [
{
"equipPoint" : "handRight",

View File

@ -454,6 +454,7 @@ Devtools for updating first person attachment rotations
(07/26/2024)
Viewmodel equipped item rotates inverted to bone rotation
Visually block
# TODO

View File

@ -4,6 +4,7 @@ import electrosphere.engine.Globals;
import electrosphere.logger.LoggerInterface;
import electrosphere.util.FileUtils;
import org.joml.Vector3d;
import org.joml.Vector3f;
/**
@ -33,6 +34,17 @@ public class AudioUtils {
return rVal;
}
/**
* Plays a audio at a given audio file path at a given position
* @param audioFile The audio file's path
* @param position The position to play it at
* @param loops If true, loops the source
* @return The audio source
*/
protected static AudioSource playAudioAtLocation(String audioFile, Vector3d position, boolean loops){
return playAudioAtLocation(audioFile,new Vector3f((float)position.x,(float)position.y,(float)position.z),loops);
}
/**
* Plays an audio file
* @param audioFile The audio file path

View File

@ -0,0 +1,352 @@
package electrosphere.entity.btree;
import java.util.function.Supplier;
import electrosphere.audio.VirtualAudioSourceManager.VirtualAudioSourceType;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.client.firstPerson.FirstPersonTree;
import electrosphere.renderer.actor.Actor;
import electrosphere.server.poseactor.PoseActor;
/**
* For a lot of behavior trees, there are states where we simply want to play an animation (ie a cooldown)
* Those states can be registered with this utility object
* When you simulate that specific state in the tree, instead ccall the simulation function from this handler
* It will handle playing the animation in first and third person, playing audio, and transitioning to the next state once the animation has completed
*/
public class StateTransitionUtil {
//The list of simplified states within this util object
StateTransitionUtilItem[] states;
//The parent entity
Entity parent;
//tracks if this is the server or not
boolean isServer;
/**
* Private constructor
* @param states
*/
private StateTransitionUtil(Entity entity, boolean isServer, StateTransitionUtilItem[] states){
this.states = states;
this.parent = entity;
this.isServer = isServer;
}
/**
* Creates a state transition util
* @param entity The entity
* @param states The states
* @return The util object
*/
public static StateTransitionUtil create(Entity entity, boolean isServer, StateTransitionUtilItem[] states){
StateTransitionUtil rVal = new StateTransitionUtil(entity,isServer,states);
//error checking
if(!isServer && Globals.clientSceneWrapper.getScene().getEntityFromId(entity.getId()) == null){
throw new IllegalArgumentException("Tried to create state transition util with isServer=false and passed in a server entity");
} else if(isServer && Globals.clientSceneWrapper.getScene().getEntityFromId(entity.getId()) != null){
throw new IllegalArgumentException("Tried to create state transition util with isServer=true and passed in a client entity");
}
return rVal;
}
/**
* Should be called every time the natural state progression is interrupted (ie if you call start() or stop())
*/
public void reset(){
for(StateTransitionUtilItem state : states){
state.startedAnimation = false;
}
}
/**
* Simulates a given state
* @param stateEnum The enum for the state
*/
public void simulate(Object stateEnum){
StateTransitionUtilItem state = null;
for(StateTransitionUtilItem targetState : states){
if(targetState.stateEnum == stateEnum){
state = targetState;
break;
}
}
if(state == null){
throw new IllegalArgumentException("Trying to simulate state that is not registered with this util object");
} else {
if(this.isServer){
simulateServerState(this.parent,state);
} else {
simulateClientState(this.parent,state);
}
}
}
/**
* Runs animation logic for client tree
* @param parent The parent entity
* @param state The state
*/
private static void simulateClientState(Entity parent, StateTransitionUtilItem state){
Actor actor = EntityUtils.getActor(parent);
if(actor != null){
//determine the third person animation to play
String animationToPlay = state.thirdPersonAnimation;
if(animationToPlay == null && state.getThirdPersonAnimation != null){
animationToPlay = state.getThirdPersonAnimation.get();
}
//determine the first person animation to play
String firstPersonAnimation = state.firstPersonAnimation;
if(firstPersonAnimation == null && state.getFirstPersonAnimation != null){
firstPersonAnimation = state.getFirstPersonAnimation.get();
}
//Main simulation
if(!actor.isPlayingAnimation() && state.onComplete != null && state.startedAnimation == true){
//state transition if this isn't set to loop
state.onComplete.run();
state.startedAnimation = false;
} else if(!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animationToPlay)){
//play animation, audio, etc, for state
if(parent == Globals.playerEntity && !Globals.controlHandler.cameraIsThirdPerson()){
//first person
//play first person audio
if(state.audioPath != null){
Globals.virtualAudioSourceManager.createVirtualAudioSource(state.audioPath, VirtualAudioSourceType.CREATURE, false);
}
FirstPersonTree.conditionallyPlayAnimation(Globals.firstPersonEntity, firstPersonAnimation);
} else {
//play third person audio
if(state.audioPath != null){
Globals.virtualAudioSourceManager.createVirtualAudioSource(state.audioPath, VirtualAudioSourceType.CREATURE, false, EntityUtils.getPosition(parent));
}
}
actor.playAnimation(animationToPlay,1);
actor.incrementAnimationTime(0.0001);
state.startedAnimation = true;
} else if(animationToPlay == null && state.onComplete != null){
state.onComplete.run();
state.startedAnimation = false;
}
}
}
/**
* Runs animation logic for server tree
* @param parent The parent entity
* @param state The state
*/
private static void simulateServerState(Entity parent, StateTransitionUtilItem state){
PoseActor poseActor = EntityUtils.getPoseActor(parent);
if(poseActor != null){
//determine the third person animation to play
String animationToPlay = state.thirdPersonAnimation;
if(animationToPlay == null && state.getThirdPersonAnimation != null){
animationToPlay = state.getThirdPersonAnimation.get();
}
//Main simulation
if(!poseActor.isPlayingAnimation() && state.onComplete != null && state.startedAnimation == true){
//state transition if this isn't set to loop
state.onComplete.run();
state.startedAnimation = false;
} else if(animationToPlay != null && (!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animationToPlay))){
//play animation for state
poseActor.playAnimation(animationToPlay,1);
poseActor.incrementAnimationTime(0.0001);
state.startedAnimation = true;
} else if(animationToPlay == null && state.onComplete != null){
state.onComplete.run();
state.startedAnimation = false;
}
}
}
/**
* A parameter used to construct a StateTransitionUtil
*/
public static class StateTransitionUtilItem {
//The enum value for this state
Object stateEnum;
//T1he animation to play in first person
String firstPersonAnimation;
//Gets the first person animation's name
Supplier<String> getFirstPersonAnimation;
//The animation to play in third person
String thirdPersonAnimation;
//Gets the third person animation's name
Supplier<String> getThirdPersonAnimation;
//The audio path
String audioPath;
//The function to fire on completion (ie to transition to the next state)
Runnable onComplete;
//Tracks whether the animation has been played or not
boolean startedAnimation = false;
/**
* Constructor
* @param stateEnum The enum value for this state
* @param firstPersonAnimation The animation to play in first person. If this is null, it will not play any animation in first person.
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
* @param audioPath The audio path to play when the animation starts. If this is null, it will not play any audio.
* @param onComplete The function to fire on completion (ie to transition to the next state). If this is null, the animation and audio will be looped on completion.
*/
public StateTransitionUtilItem(
Object stateEnum,
String firstPersonAnimation,
String thirdPersonAnimation,
String audioPath,
Runnable onComplete
){
this.stateEnum = stateEnum;
this.firstPersonAnimation = firstPersonAnimation;
this.thirdPersonAnimation = thirdPersonAnimation;
this.audioPath = audioPath;
this.onComplete = onComplete;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param firstPersonAnimation The animation to play in first person. If this is null, it will not play any animation in first person.
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
* @param onComplete The function to fire on completion (ie to transition to the next state). If this is null, the animation and audio will be looped on completion.
*/
public StateTransitionUtilItem(
Object stateEnum,
String firstPersonAnimation,
String thirdPersonAnimation,
Runnable onComplete
){
this.stateEnum = stateEnum;
this.firstPersonAnimation = firstPersonAnimation;
this.thirdPersonAnimation = thirdPersonAnimation;
this.onComplete = onComplete;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param firstPersonAnimation The animation to play in first person. If this is null, it will not play any animation in first person.
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
* @param audioPath The audio path to play when the animation starts. If this is null, it will not play any audio.
* @param onComplete The function to fire on completion (ie to transition to the next state). If this is null, the animation and audio will be looped on completion.
*/
public StateTransitionUtilItem(
Object stateEnum,
Supplier<String> getFirstPersonAnimation,
Supplier<String> getThirdPersonAnimation,
String audioPath,
Runnable onComplete
){
this.stateEnum = stateEnum;
this.getFirstPersonAnimation = getFirstPersonAnimation;
this.getThirdPersonAnimation = getThirdPersonAnimation;
this.audioPath = audioPath;
this.onComplete = onComplete;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param firstPersonAnimation The animation to play in first person. If this is null, it will not play any animation in first person.
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
* @param onComplete The function to fire on completion (ie to transition to the next state). If this is null, the animation and audio will be looped on completion.
*/
public StateTransitionUtilItem(
Object stateEnum,
Supplier<String> getFirstPersonAnimation,
Supplier<String> getThirdPersonAnimation,
Runnable onComplete
){
this.stateEnum = stateEnum;
this.getFirstPersonAnimation = getFirstPersonAnimation;
this.getThirdPersonAnimation = getThirdPersonAnimation;
this.onComplete = onComplete;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
* @param onComplete The function to fire on completion (ie to transition to the next state). If this is null, the animation and audio will be looped on completion.
*/
public StateTransitionUtilItem(
Object stateEnum,
Supplier<String> getThirdPersonAnimation,
Runnable onComplete
){
this.stateEnum = stateEnum;
this.getThirdPersonAnimation = getThirdPersonAnimation;
this.onComplete = onComplete;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param firstPersonAnimation The animation to play in first person. If this is null, it will not play any animation in first person.
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
*/
public StateTransitionUtilItem(
Object stateEnum,
Supplier<String> getFirstPersonAnimation,
Supplier<String> getThirdPersonAnimation
){
this.stateEnum = stateEnum;
this.getFirstPersonAnimation = getFirstPersonAnimation;
this.getThirdPersonAnimation = getThirdPersonAnimation;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
*/
public StateTransitionUtilItem(
Object stateEnum,
Supplier<String> getThirdPersonAnimation
){
this.stateEnum = stateEnum;
this.getThirdPersonAnimation = getThirdPersonAnimation;
}
/**
* Constructor
* @param stateEnum The enum value for this state
* @param thirdPersonAnimation The animation to play in third person. If this is null, it will not play any animation in third person.
* @param onComplete The function to fire on completion (ie to transition to the next state). If this is null, the animation and audio will be looped on completion.
*/
public StateTransitionUtilItem(
Object stateEnum,
String thirdPersonAnimation,
Runnable onComplete
){
this.stateEnum = stateEnum;
this.thirdPersonAnimation = thirdPersonAnimation;
this.onComplete = onComplete;
}
}
}

View File

@ -4,15 +4,15 @@ package electrosphere.entity.state.block;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.btree.BehaviorTree;
import electrosphere.entity.btree.StateTransitionUtil;
import electrosphere.entity.btree.StateTransitionUtil.StateTransitionUtilItem;
import electrosphere.game.data.creature.type.block.BlockSystem;
import electrosphere.net.synchronization.BehaviorTreeIdEnums;
import electrosphere.net.synchronization.annotation.SyncedField;
import electrosphere.net.synchronization.annotation.SynchronizableEnum;
import electrosphere.net.synchronization.annotation.SynchronizedBehaviorTree;
import electrosphere.renderer.actor.Actor;
@SynchronizedBehaviorTree(name = "clientBlockTree", isServer = false, correspondingTree="serverBlockTree")
/**
@ -43,50 +43,45 @@ public class ClientBlockTree implements BehaviorTree {
//The data for block animations
BlockSystem blockSystem;
//The state transition util
StateTransitionUtil stateTransitionUtil;
/**
* Constructor
*/
private ClientBlockTree(Entity parent, BlockSystem blockSystem){
this.parent = parent;
this.blockSystem = blockSystem;
this.stateTransitionUtil = StateTransitionUtil.create(parent, false, new StateTransitionUtilItem[]{
new StateTransitionUtilItem(
BlockState.WIND_UP,
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getWindUpFirstPersonAnimation();},
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getWindUpAnimation();}
),
new StateTransitionUtilItem(
BlockState.BLOCKING,
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getMainFirstPersonAnimation();},
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getMainAnimation();}
),
new StateTransitionUtilItem(
BlockState.COOLDOWN,
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getCooldownFirstPersonAnimation();},
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getCooldownAnimation();}
),
});
}
@Override
public void simulate(float deltaTime) {
Actor actor = EntityUtils.getActor(parent);
switch(state){
case WIND_UP: {
if(actor != null && blockSystem.getBlockVariant(currentBlockVariant) != null){
String animationToPlay = blockSystem.getBlockVariant(currentBlockVariant).getWindUpAnimation();
if(
!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animationToPlay)
){
actor.playAnimation(animationToPlay,1);
actor.incrementAnimationTime(0.0001);
}
}
this.stateTransitionUtil.simulate(BlockState.WIND_UP);
} break;
case BLOCKING: {
if(actor != null && blockSystem.getBlockVariant(currentBlockVariant) != null){
String animationToPlay = blockSystem.getBlockVariant(currentBlockVariant).getMainAnimation();
if(
!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animationToPlay)
){
actor.playAnimation(animationToPlay,1);
actor.incrementAnimationTime(0.0001);
}
}
this.stateTransitionUtil.simulate(BlockState.BLOCKING);
} break;
case COOLDOWN: {
if(actor != null && blockSystem.getBlockVariant(currentBlockVariant) != null){
String animationToPlay = blockSystem.getBlockVariant(currentBlockVariant).getCooldownAnimation();
if(
!actor.isPlayingAnimation() || !actor.isPlayingAnimation(animationToPlay)
){
actor.playAnimation(animationToPlay,1);
actor.incrementAnimationTime(0.0001);
}
}
this.stateTransitionUtil.simulate(BlockState.COOLDOWN);
} break;
case NOT_BLOCKING: {

View File

@ -5,12 +5,12 @@ import electrosphere.net.synchronization.FieldIdEnums;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.btree.BehaviorTree;
import electrosphere.entity.btree.StateTransitionUtil;
import electrosphere.entity.btree.StateTransitionUtil.StateTransitionUtilItem;
import electrosphere.net.synchronization.BehaviorTreeIdEnums;
import electrosphere.server.datacell.utils.ServerBehaviorTreeUtils;
import electrosphere.server.poseactor.PoseActor;
import electrosphere.net.parser.net.message.SynchronizationMessage;
import electrosphere.server.datacell.utils.DataCellSearchUtils;
@ -38,18 +38,38 @@ public class ServerBlockTree implements BehaviorTree {
//The data for block animations
BlockSystem blockSystem;
//The state transition util
StateTransitionUtil stateTransitionUtil;
/**
* Constructor
*/
private ServerBlockTree(Entity parent, BlockSystem blockSystem){
this.parent = parent;
this.blockSystem = blockSystem;
this.stateTransitionUtil = StateTransitionUtil.create(parent, true, new StateTransitionUtilItem[]{
new StateTransitionUtilItem(
BlockState.WIND_UP,
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getWindUpAnimation();},
() -> {this.setState(BlockState.BLOCKING);}
),
new StateTransitionUtilItem(
BlockState.BLOCKING,
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getMainAnimation();}
),
new StateTransitionUtilItem(
BlockState.COOLDOWN,
() -> {return this.blockSystem.getBlockVariant(this.currentBlockVariant).getCooldownAnimation();},
() -> {this.setState(BlockState.NOT_BLOCKING);}
),
});
}
/**
* Starts the block tree
*/
public void start(){
this.stateTransitionUtil.reset();
setState(BlockState.WIND_UP);
}
@ -57,45 +77,21 @@ public class ServerBlockTree implements BehaviorTree {
* Stops the block tree
*/
public void stop(){
this.stateTransitionUtil.reset();
setState(BlockState.COOLDOWN);
}
@Override
public void simulate(float deltaTime) {
PoseActor poseActor = EntityUtils.getPoseActor(parent);
switch(state){
case WIND_UP: {
if(poseActor != null && blockSystem.getBlockVariant(currentBlockVariant) != null){
String animationToPlay = blockSystem.getBlockVariant(currentBlockVariant).getWindUpAnimation();
if(
!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animationToPlay)
){
poseActor.playAnimation(animationToPlay,1);
poseActor.incrementAnimationTime(0.0001);
}
}
this.stateTransitionUtil.simulate(BlockState.WIND_UP);
} break;
case BLOCKING: {
if(poseActor != null && blockSystem.getBlockVariant(currentBlockVariant) != null){
String animationToPlay = blockSystem.getBlockVariant(currentBlockVariant).getMainAnimation();
if(
!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animationToPlay)
){
poseActor.playAnimation(animationToPlay,1);
poseActor.incrementAnimationTime(0.0001);
}
}
this.stateTransitionUtil.simulate(BlockState.BLOCKING);
} break;
case COOLDOWN: {
if(poseActor != null && blockSystem.getBlockVariant(currentBlockVariant) != null){
String animationToPlay = blockSystem.getBlockVariant(currentBlockVariant).getCooldownAnimation();
if(
!poseActor.isPlayingAnimation() || !poseActor.isPlayingAnimation(animationToPlay)
){
poseActor.playAnimation(animationToPlay,1);
poseActor.incrementAnimationTime(0.0001);
}
}
this.stateTransitionUtil.simulate(BlockState.COOLDOWN);
} break;
case NOT_BLOCKING: {

View File

@ -21,6 +21,15 @@ public class BlockVariant {
//the animation to play when cooling down
String cooldownAnimation;
//the animation to play in first person when winding up
String windUpFirstPersonAnimation;
//the main animation to play in first person while blocking
String mainFirstPersonAnimation;
//the animation to play in first person when cooling down
String cooldownFirstPersonAnimation;
//the list of default equipment cases that this variant should be used for
List<VariantDefaults> defaults;
@ -56,6 +65,30 @@ public class BlockVariant {
return cooldownAnimation;
}
/**
* The animation to play in first person when winding up
* @return
*/
public String getWindUpFirstPersonAnimation(){
return windUpFirstPersonAnimation;
}
/**
* The main animation to play in first person while blocking
* @return
*/
public String getMainFirstPersonAnimation(){
return mainFirstPersonAnimation;
}
/**
* The animation to play in first person when cooling down
* @return
*/
public String getCooldownFirstPersonAnimation(){
return cooldownFirstPersonAnimation;
}
/**
* the list of default equipment cases that this variant should be used for
* @return