From 1ca9c60640c3d505e7aeb102e66c3e612f2a8c76 Mon Sep 17 00:00:00 2001 From: austin Date: Tue, 13 Aug 2024 15:51:46 -0400 Subject: [PATCH] hitbox collision debounce work --- docs/src/progress/renderertodo.md | 3 + .../audio/collision/HitboxAudioService.java | 2 +- .../entity/state/attack/ServerAttackTree.java | 62 ++++++-- .../entity/state/life/ServerLifeTree.java | 141 +++++++++++++++++ .../ServerHitboxResolutionCallback.java | 142 +++++++++++------- 5 files changed, 282 insertions(+), 68 deletions(-) diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index df24443f..7b30046f 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -553,6 +553,7 @@ Movement tweaks Hitbox support offsets now Multiple hitboxes per bone Potential fix for client concurrency issue +Debounce attack collisions # TODO @@ -561,6 +562,8 @@ Potential fix for client concurrency issue BIG BIG BIG BIG IMMEDIATE TO DO: always enforce opengl interface across all opengl calls jesus christ the bone uniform bug was impossible +Rename "BehaviorTree" to be "Component" (what it actually is) + Ability to fully reload game engine state without exiting client - Back out to main menu and load a new level without any values persisting - Receive a teleport packet from server and flush all game state before requesting state from server again diff --git a/src/main/java/electrosphere/audio/collision/HitboxAudioService.java b/src/main/java/electrosphere/audio/collision/HitboxAudioService.java index 6681b157..843598f3 100644 --- a/src/main/java/electrosphere/audio/collision/HitboxAudioService.java +++ b/src/main/java/electrosphere/audio/collision/HitboxAudioService.java @@ -119,7 +119,7 @@ public class HitboxAudioService { if(ItemUtils.isWeapon(senderEntity)){ if(CreatureUtils.isCreature(receiverEntity)){ if(isBlockSound){ - return this.getWeaponOnCreature(); + return this.getWeaponOnBlock(); } else if(isDamageSound){ return this.getWeaponOnCreature(); } diff --git a/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java b/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java index b86fe703..1cb4e76b 100644 --- a/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java +++ b/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java @@ -37,6 +37,7 @@ import electrosphere.renderer.actor.Actor; import electrosphere.server.datacell.Realm; import electrosphere.util.math.MathUtils; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -82,6 +83,11 @@ public class ServerAttackTree implements BehaviorTree { String projectileToFire = null; String attackingPoint = null; + /** + * The list of entities that have collided with the current attack + */ + List collidedEntities = new LinkedList(); + //The state transition util StateTransitionUtil stateTransitionUtil; @@ -190,14 +196,14 @@ public class ServerAttackTree implements BehaviorTree { //figure out attack type we should be doing String attackType = getAttackType(); //if we can attack, setup doing so - if(canAttack(attackType)){ - setAttackMoveTypeActive(attackType); - currentMoveset = getMoveset(attackType); + if(this.canAttack(attackType)){ + this.setAttackMoveTypeActive(attackType); + currentMoveset = this.getMoveset(attackType); if(currentMoveset != null){ if(currentMove == null){ currentMove = currentMoveset.get(0); } else { - currentMove = getNextMove(currentMoveset,currentMove.getNextMoveId()); + currentMove = this.getNextMove(currentMoveset,currentMove.getNextMoveId()); } if(currentMove != null){ firesProjectile = currentMove.getFiresProjectile(); @@ -206,18 +212,20 @@ public class ServerAttackTree implements BehaviorTree { } //set initial stuff (this alerts the client as well) - setCurrentMoveId(currentMove.getAttackMoveId()); + this.setCurrentMoveId(currentMove.getAttackMoveId()); //start tree if(currentMove.getWindupState() != null){ - setState(AttackTreeState.WINDUP); + this.setState(AttackTreeState.WINDUP); } else if(currentMove.getAttackState() != null){ - setState(AttackTreeState.ATTACK); + this.setState(AttackTreeState.ATTACK); } else { LoggerInterface.loggerEngine.ERROR(new IllegalStateException("Trying to start attacking tree, but current move does not have windup or attack states defined!")); } //intuit can hold from presence of windup anim currentMoveCanHold = currentMove.getHoldState() != null; + //clear collided list + this.collidedEntities.clear(); //stop movement tree if(parent.containsKey(EntityDataStrings.SERVER_MOVEMENT_BT)){ BehaviorTree movementTree = CreatureUtils.serverGetEntityMovementTree(parent); @@ -229,12 +237,15 @@ public class ServerAttackTree implements BehaviorTree { EntityUtils.getRotation(parent).rotationTo(MathUtils.getOriginVector(), new Vector3d(movementVector.x,movementVector.y,movementVector.z)); frameCurrent = 0; } else { - setState(AttackTreeState.IDLE); + this.setState(AttackTreeState.IDLE); } } } } + /** + * Releases the tree from holding its animation + */ public void release(){ stillHold = false; } @@ -429,10 +440,18 @@ public class ServerAttackTree implements BehaviorTree { } } + /** + * Adds a network message to the tree + * @param networkMessage The network message + */ public void addNetworkMessage(EntityMessage networkMessage) { networkMessageQueue.add(networkMessage); } + /** + * Gets the current attack type + * @return The current attack type + */ String getAttackType(){ String rVal = null; if(ServerEquipState.hasEquipState(parent)){ @@ -464,7 +483,7 @@ public class ServerAttackTree implements BehaviorTree { * @param attackType The type of attack to perform * @return true if it can attack, false otherwise */ - boolean canAttack(String attackType){ + private boolean canAttack(String attackType){ boolean rVal = true; if(attackType == null){ return false; @@ -503,7 +522,13 @@ public class ServerAttackTree implements BehaviorTree { return rVal; } - AttackMove getNextMove(List moveset, String nextMoveId){ + /** + * Gets the next attack move + * @param moveset The moveset + * @param nextMoveId The next move's id + * @return The next move if it exists, null otherwise + */ + private AttackMove getNextMove(List moveset, String nextMoveId){ AttackMove rVal = null; for(AttackMove move : moveset){ if(move.getAttackMoveId().equals(nextMoveId)){ @@ -538,6 +563,23 @@ public class ServerAttackTree implements BehaviorTree { public List getMoveset(String attackType){ return (List)parent.getData(attackType); } + + /** + * Checks if the target can be collided with + * @param target The target + * @return true if can be collided with, false otherwise (ie it has already collided) + */ + public boolean canCollideEntity(Entity target){ + return !this.collidedEntities.contains(target); + } + + /** + * Sets that the current attack has collided with the provided entity + * @param target The target entity + */ + public void collideEntity(Entity target){ + this.collidedEntities.add(target); + } /** *

Automatically generated

diff --git a/src/main/java/electrosphere/entity/state/life/ServerLifeTree.java b/src/main/java/electrosphere/entity/state/life/ServerLifeTree.java index 4ff8bbb1..3010e192 100644 --- a/src/main/java/electrosphere/entity/state/life/ServerLifeTree.java +++ b/src/main/java/electrosphere/entity/state/life/ServerLifeTree.java @@ -7,12 +7,22 @@ import electrosphere.entity.btree.StateTransitionUtil.StateTransitionUtilItem; import electrosphere.entity.EntityDataStrings; import electrosphere.entity.Entity; import electrosphere.server.datacell.utils.ServerBehaviorTreeUtils; +import electrosphere.net.parser.net.message.CombatMessage; import electrosphere.net.parser.net.message.SynchronizationMessage; import electrosphere.server.datacell.utils.DataCellSearchUtils; +import java.util.LinkedList; +import java.util.List; + +import org.joml.Vector3d; + import electrosphere.engine.Globals; +import electrosphere.entity.state.hitbox.HitboxCollectionState.HitboxState; import electrosphere.entity.state.life.ClientLifeTree.LifeStateEnum; +import electrosphere.entity.types.item.ItemUtils; +import electrosphere.game.data.collidable.HitboxData; import electrosphere.game.data.creature.type.HealthSystem; +import electrosphere.logger.LoggerInterface; import electrosphere.net.synchronization.annotation.SyncedField; import electrosphere.net.synchronization.annotation.SynchronizedBehaviorTree; import electrosphere.net.synchronization.enums.BehaviorTreeIdEnums; @@ -48,8 +58,12 @@ public class ServerLifeTree implements BehaviorTree { //the current iframe count int iFrameCurrent = 0; + //accumulates collisions and determines if the parent takes damage or blocks them + List collisionAccumulator = new LinkedList(); + @Override public void simulate(float deltaTime) { + this.handleAccumulatedCollisions(); switch(state){ case ALIVE: { if(iFrameCurrent > 0){ @@ -101,6 +115,85 @@ public class ServerLifeTree implements BehaviorTree { return this.state == LifeStateEnum.ALIVE; } + /** + * Handles the collisions that have been accumulated in this tree + */ + private void handleAccumulatedCollisions(){ + int numCollisions = 0; + if(collisionAccumulator.size() > 0){ + //get the blocked entities + List blockedEntities = new LinkedList(); + for(CollisionEvent event : collisionAccumulator){ + if(event.isBlock && !blockedEntities.contains(event.source)){ + //don't allow multiple hits per collision + blockedEntities.add(event.source); + + //tracking + numCollisions++; + + //tell clients an impact just happened + DataCellSearchUtils.getEntityDataCell(parent).broadcastNetworkMessage( + CombatMessage.constructserverReportHitboxCollisionMessage( + event.source.getId(), + parent.getId(), + Globals.timekeeper.getNumberOfSimFramesElapsed(), + event.sourceHitboxData.getHitboxData().getType(), + HitboxData.HITBOX_TYPE_BLOCK_CONNECTED + ) + ); + } + } + for(CollisionEvent event : collisionAccumulator){ + if(event.isDamage && !blockedEntities.contains(event.source)){ + //don't allow multiple hits per collision + blockedEntities.add(event.source); + + //tracking + numCollisions++; + + //tell clients an impact just happened + DataCellSearchUtils.getEntityDataCell(parent).broadcastNetworkMessage( + CombatMessage.constructserverReportHitboxCollisionMessage( + event.source.getId(), + parent.getId(), + Globals.timekeeper.getNumberOfSimFramesElapsed(), + event.sourceHitboxData.getHitboxData().getType(), + event.parentHitboxData.getHitboxData().getType() + ) + ); + + //do damage calculation + int damage = ItemUtils.getWeaponDataRaw(event.source).getDamage(); + this.damage(damage); + if(!this.isAlive()){ + throw new UnsupportedOperationException("Reviving not implemented yet!"); + // Realm entityRealm = Globals.realmManager.getEntityRealm(receiverParent); + // EntityUtils.getPosition(receiverParent).set(entityRealm.getSpawnPoint()); + // serverLifeTree.revive(); + } + } + } + collisionAccumulator.clear(); + } + if(numCollisions > 0){ + LoggerInterface.loggerEngine.DEBUG("Server life tree handled: " + numCollisions + " unique collisions"); + } + } + + /** + * Adds a collision event to the life tree + * @param collisionSource The collision source + * @param position The position of the collision event + * @param sourceHitboxData The hitbox data for the source of the collision + * @param parentHitboxData The hitbox data for the parent of the tree + * @param isDamage True if this is a damage event + * @param isBlock True if this is a block event + */ + public void addCollisionEvent(Entity collisionSource, HitboxState sourceHitboxData, HitboxState parentHitboxData, Vector3d position, boolean isDamage, boolean isBlock){ + CollisionEvent collisionEvent = new CollisionEvent(collisionSource, sourceHitboxData, parentHitboxData, isDamage, isBlock); + this.collisionAccumulator.add(collisionEvent); + } + /** *

Automatically generated

*

@@ -192,4 +285,52 @@ public class ServerLifeTree implements BehaviorTree { return (ServerLifeTree)entity.getData(EntityDataStrings.TREE_SERVERLIFETREE); } + /** + * A single collision event + */ + public static class CollisionEvent { + + /** + * The source of the collision event + */ + Entity source; + + /** + * The hitbox data for the source of the collision + */ + HitboxState sourceHitboxData; + + /** + * The hitbox data for the parent of the tree + */ + HitboxState parentHitboxData; + + /** + * True if this is a damage event + */ + boolean isDamage; + + /** + * True if this is a block event + */ + boolean isBlock; + + /** + * Constructor + * @param source The source of the collision + * @param sourceHitboxData The hitbox data for the source of the collision + * @param parentHitboxData The hitbox data for the parent of the tree + * @param isDamage True if this is a damage event + * @param isBlock True if this is a block event + */ + public CollisionEvent(Entity source, HitboxState sourceHitboxData, HitboxState parentHitboxData, boolean isDamage, boolean isBlock){ + this.source = source; + this.sourceHitboxData = sourceHitboxData; + this.parentHitboxData = parentHitboxData; + this.isDamage = isDamage; + this.isBlock = isBlock; + } + + } + } diff --git a/src/main/java/electrosphere/server/datacell/physics/ServerHitboxResolutionCallback.java b/src/main/java/electrosphere/server/datacell/physics/ServerHitboxResolutionCallback.java index 3c16060e..642421de 100644 --- a/src/main/java/electrosphere/server/datacell/physics/ServerHitboxResolutionCallback.java +++ b/src/main/java/electrosphere/server/datacell/physics/ServerHitboxResolutionCallback.java @@ -6,19 +6,14 @@ import org.ode4j.ode.DGeom; import electrosphere.collision.CollisionEngine.CollisionResolutionCallback; import electrosphere.collision.collidable.Collidable; -import electrosphere.engine.Globals; import electrosphere.entity.Entity; +import electrosphere.entity.state.attack.ServerAttackTree; import electrosphere.entity.state.hitbox.HitboxCollectionState; import electrosphere.entity.state.hitbox.HitboxCollectionState.HitboxState; import electrosphere.entity.state.hitbox.HitboxCollectionState.HitboxType; import electrosphere.entity.state.life.ServerLifeTree; -import electrosphere.entity.state.movement.ProjectileTree; import electrosphere.entity.types.attach.AttachUtils; -import electrosphere.entity.types.creature.CreatureUtils; import electrosphere.entity.types.item.ItemUtils; -import electrosphere.game.data.collidable.HitboxData; -import electrosphere.net.parser.net.message.CombatMessage; -import electrosphere.server.datacell.utils.DataCellSearchUtils; /** * Callback for managing collisions on the server @@ -43,22 +38,49 @@ public class ServerHitboxResolutionCallback implements CollisionResolutionCallba boolean receiverIsHurt = receiverShapeStatus != null && receiverShapeStatus.getType() == HitboxType.HURT; boolean receiverIsBlock = receiverShapeStatus != null && (receiverShapeStatus.getType() == HitboxType.BLOCK || (receiverShapeStatus.getType() == HitboxType.HIT && receiverShapeStatus.isBlockOverride())); boolean parentsAreDifferent = AttachUtils.getParent(impactorParent) != receiverParent; + boolean impactorCollisionBlocked = false; - //currently, impactor needs to be an item, and the receiver must not be an item + //check if the impactor thinks it can collide with the receiver + if(impactorParent != null && ServerAttackTree.getServerAttackTree(impactorParent) != null){ + ServerAttackTree impactorAttackTree = ServerAttackTree.getServerAttackTree(impactorParent); + //if we collide with the creature directly + if(!impactorAttackTree.canCollideEntity(receiverParent)){ + impactorCollisionBlocked = true; + } + //if we collide with an item attached to the creature + if(AttachUtils.hasParent(receiverParent) && !impactorAttackTree.canCollideEntity(AttachUtils.getParent(receiverParent))){ + impactorCollisionBlocked = true; + } + } else if(impactorParent != null && AttachUtils.hasParent(impactorParent) && AttachUtils.getParent(impactorParent) != null && ServerAttackTree.getServerAttackTree(AttachUtils.getParent(impactorParent)) != null){ + ServerAttackTree impactorAttackTree = ServerAttackTree.getServerAttackTree(AttachUtils.getParent(impactorParent)); + //if we collide with the creature directly + if(!impactorAttackTree.canCollideEntity(receiverParent)){ + impactorCollisionBlocked = true; + } + //if we collide with an item attached to the creature + if(AttachUtils.hasParent(receiverParent) && !impactorAttackTree.canCollideEntity(AttachUtils.getParent(receiverParent))){ + impactorCollisionBlocked = true; + } + } + + //check if is damage event boolean isDamageEvent = !impactorShapeStatusIsNull && !receiverShapeStatusIsNull && impactorIsHit && receiverIsHurt && - parentsAreDifferent + parentsAreDifferent && + !impactorCollisionBlocked ; + //check if is block event boolean isBlockEvent = !impactorShapeStatusIsNull && !receiverShapeStatusIsNull && impactorIsHit && receiverIsBlock && - parentsAreDifferent + parentsAreDifferent && + !impactorCollisionBlocked ; @@ -75,62 +97,68 @@ public class ServerHitboxResolutionCallback implements CollisionResolutionCallba boolean isItem = ItemUtils.isItem(impactorParent);//hitboxParent.containsKey(EntityDataStrings.ITEM_IS_ITEM); Entity hitboxAttachParent = AttachUtils.getParent(impactorParent); - //tell clients an impact just happened - DataCellSearchUtils.getEntityDataCell(receiverParent).broadcastNetworkMessage( - CombatMessage.constructserverReportHitboxCollisionMessage( - impactorParent.getId(), - receiverParent.getId(), - Globals.timekeeper.getNumberOfSimFramesElapsed(), - impactorShapeStatus.getHitboxData().getType(), - receiverShapeStatus.getHitboxData().getType() - ) - ); - + // + //handle receiver if(isItem){ if(hitboxAttachParent != receiverParent){ - int damage = ItemUtils.getWeaponDataRaw(impactorParent).getDamage(); ServerLifeTree serverLifeTree = ServerLifeTree.getServerLifeTree(receiverParent); - serverLifeTree.damage(damage); - if(!serverLifeTree.isAlive()){ - throw new UnsupportedOperationException("Reviving not implemented yet!"); - // Realm entityRealm = Globals.realmManager.getEntityRealm(receiverParent); - // EntityUtils.getPosition(receiverParent).set(entityRealm.getSpawnPoint()); - // serverLifeTree.revive(); - } - } - } else { - int damage = 0; - //for entities using attacktree - if(CreatureUtils.serverGetAttackTree(impactorParent) != null){ - damage = ItemUtils.getWeaponDataRaw(impactorParent).getDamage(); - } else { - //for entities using shooter tree - if(ProjectileTree.getProjectileTree(impactorParent) != null){ - damage = (int)ProjectileTree.getProjectileTree(impactorParent).getDamage(); - } - } - ServerLifeTree serverLifeTree = ServerLifeTree.getServerLifeTree(receiverParent); - serverLifeTree.damage(damage); - if(!serverLifeTree.isAlive()){ - throw new UnsupportedOperationException("Reviving not implemented yet!"); - // Realm entityRealm = Globals.realmManager.getEntityRealm(receiverParent); - // EntityUtils.getPosition(receiverParent).set(entityRealm.getSpawnPoint()); - // serverLifeTree.revive(); + serverLifeTree.addCollisionEvent(impactorParent, impactorShapeStatus, receiverShapeStatus, localPosition, isDamageEvent, isBlockEvent); } } + + // + //handle attacker + this.handleAttackerCollision(impactorParent,receiverParent); } if(isBlockEvent){ - //tell clients an impact just happened - DataCellSearchUtils.getEntityDataCell(receiverParent).broadcastNetworkMessage( - CombatMessage.constructserverReportHitboxCollisionMessage( - impactorParent.getId(), - receiverParent.getId(), - Globals.timekeeper.getNumberOfSimFramesElapsed(), - impactorShapeStatus.getHitboxData().getType(), - HitboxData.HITBOX_TYPE_BLOCK_CONNECTED //TODO: more proper block override handling - ) - ); + // + //handle receiver + boolean receiverIsItem = ItemUtils.isItem(receiverParent); + boolean receiverHasParent = AttachUtils.hasParent(receiverParent); + if(receiverIsItem && receiverHasParent){ + //item is equipped to something + ServerLifeTree serverLifeTree = ServerLifeTree.getServerLifeTree(AttachUtils.getParent(receiverParent)); + if(serverLifeTree != null){ + serverLifeTree.addCollisionEvent(impactorParent, impactorShapeStatus, receiverShapeStatus, localPosition, isDamageEvent, isBlockEvent); + } + } else { + //attacking an item that is not equipped to anything + ServerLifeTree serverLifeTree = ServerLifeTree.getServerLifeTree(receiverParent); + if(serverLifeTree != null){ + serverLifeTree.addCollisionEvent(impactorParent, impactorShapeStatus, receiverShapeStatus, localPosition, isDamageEvent, isBlockEvent); + } + } + + // + //handle attacker + this.handleAttackerCollision(impactorParent,receiverParent); + } + } + + /** + * Handles collision tracking from the impactor's side + * @param impactorParent The impactor hitbox's parent entity + * @param receiverParent The receiver hitbox's parent entity + */ + private void handleAttackerCollision(Entity impactorParent, Entity receiverParent){ + boolean receiverIsItem = ItemUtils.isItem(receiverParent); + boolean receiverHasParent = AttachUtils.hasParent(receiverParent); + + if(impactorParent != null && ServerAttackTree.getServerAttackTree(impactorParent) != null){ + ServerAttackTree impactorAttackTree = ServerAttackTree.getServerAttackTree(impactorParent); + impactorAttackTree.collideEntity(receiverParent); + //if the receiver is an item that is equipped, collide with parent too + if(receiverIsItem && receiverHasParent){ + impactorAttackTree.collideEntity(AttachUtils.getParent(receiverParent)); + } + } else if(impactorParent != null && AttachUtils.hasParent(impactorParent) && AttachUtils.getParent(impactorParent) != null && ServerAttackTree.getServerAttackTree(AttachUtils.getParent(impactorParent)) != null){ + ServerAttackTree impactorAttackTree = ServerAttackTree.getServerAttackTree(AttachUtils.getParent(impactorParent)); + impactorAttackTree.collideEntity(receiverParent); + //if the receiver is an item that is equipped, collide with parent too + if(receiverIsItem && receiverHasParent){ + impactorAttackTree.collideEntity(AttachUtils.getParent(receiverParent)); + } } }