hitbox collision debounce work
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2024-08-13 15:51:46 -04:00
parent 4cf34d162c
commit 1ca9c60640
5 changed files with 282 additions and 68 deletions

View File

@ -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

View File

@ -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();
}

View File

@ -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<Entity> collidedEntities = new LinkedList<Entity>();
//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<AttackMove> 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<AttackMove> moveset, String nextMoveId){
AttackMove rVal = null;
for(AttackMove move : moveset){
if(move.getAttackMoveId().equals(nextMoveId)){
@ -539,6 +564,23 @@ public class ServerAttackTree implements BehaviorTree {
return (List<AttackMove>)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);
}
/**
* <p> Automatically generated </p>
* <p>

View File

@ -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<CollisionEvent> collisionAccumulator = new LinkedList<CollisionEvent>();
@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<Entity> blockedEntities = new LinkedList<Entity>();
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);
}
/**
* <p> Automatically generated </p>
* <p>
@ -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;
}
}
}

View File

@ -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));
}
}
}