Renderer/src/main/java/electrosphere/entity/state/hitbox/HitboxCollectionState.java
austin 06b068643e
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
hitbox offsets
2024-08-13 12:39:53 -04:00

730 lines
28 KiB
Java

package electrosphere.entity.state.hitbox;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.ode4j.ode.DBody;
import org.ode4j.ode.DGeom;
import electrosphere.collision.CollisionBodyCreation;
import electrosphere.collision.CollisionEngine;
import electrosphere.collision.PhysicsEntityUtils;
import electrosphere.collision.PhysicsUtils;
import electrosphere.collision.collidable.Collidable;
import electrosphere.collision.hitbox.HitboxManager;
import electrosphere.collision.hitbox.HitboxUtils.HitboxPositionCallback;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.hitbox.HitboxCollectionState.HitboxState.HitboxShapeType;
import electrosphere.entity.types.attach.AttachUtils;
import electrosphere.game.data.collidable.HitboxData;
import electrosphere.game.data.utils.DataFormatUtil;
import electrosphere.logger.LoggerInterface;
import electrosphere.util.math.MathUtils;
/**
* The state of the collection of all hitboxes on this entity
* Ie, it stores the state of each hitbox that is attached to this entity
*/
public class HitboxCollectionState {
/**
* Types of hitboxes
*/
public enum HitboxType {
HIT, // damages another entity
HURT, // receives damage from another entity
BLOCK, // blocks a hit from another entity
}
/**
* The subtype of hitbox
*/
public enum HitboxSubtype {
SWEET, //extra damage
REGULAR, //regular damage
SOUR, //less damage
}
//the parent entity of the hitbox state
Entity parent;
//the body that contains all the hitbox shapes
DBody body;
//The collidable associated with the body
Collidable collidable;
//the list of all geoms in the collection state
List<DGeom> geoms = new LinkedList<DGeom>();
//the map of bone -> hitbox shape in ode4j
Map<String,DGeom> hitboxGeomMap = new HashMap<String,DGeom>();
//the map of geometry -> hitbox shape status, useful for finding data about a given hitbox during collision
Map<DGeom,HitboxState> geomStateMap = new HashMap<DGeom,HitboxState>();
//callback to provide a position for the hitbox each frame
HitboxPositionCallback positionCallback;
//controls whether the hitbox state is active or not
boolean active = true;
//controls whether active hitboxes should be overwritten with block boxes
boolean blockOverride = false;
//the associated manager
HitboxManager manager;
//controls whether this hitbox collection thinks its on the server or client
boolean isServer = true;
/**
* Create hitbox state for an entity
* @param collisionEngine the collision engine
* @param entity The entity to attach the state to
* @param hitboxListRaw The list of hitbox data to apply
* @return The hitbox state that has been attached to the entity
*/
public static HitboxCollectionState attachHitboxState(HitboxManager manager, boolean isServer, Entity entity, List<HitboxData> hitboxListRaw){
HitboxCollectionState rVal = new HitboxCollectionState();
rVal.isServer = isServer;
//create the shapes
for(HitboxData hitboxDataRaw : hitboxListRaw){
DGeom geom = null;
HitboxType type = HitboxType.HIT;
HitboxShapeType shapeType = HitboxShapeType.SPHERE;
//
//Get the type as an enum
//
switch(hitboxDataRaw.getType()){
case HitboxData.HITBOX_TYPE_HIT: {
type = HitboxType.HIT;
shapeType = HitboxShapeType.SPHERE;
geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT);
} break;
case HitboxData.HITBOX_TYPE_HURT: {
type = HitboxType.HURT;
shapeType = HitboxShapeType.SPHERE;
geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT);
} break;
case HitboxData.HITBOX_TYPE_HIT_CONNECTED: {
type = HitboxType.HIT;
shapeType = HitboxShapeType.CAPSULE;
geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT);
} break;
case HitboxData.HITBOX_TYPE_HURT_CONNECTED: {
type = HitboxType.HURT;
shapeType = HitboxShapeType.CAPSULE;
geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT);
} break;
case HitboxData.HITBOX_TYPE_STATIC_CAPSULE: {
type = HitboxType.HURT;
shapeType = HitboxShapeType.STATIC_CAPSULE;
geom = CollisionBodyCreation.createCapsuleShape(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), hitboxDataRaw.getLength(), Collidable.TYPE_OBJECT_BIT);
} break;
}
//
//Get the subtype as an enum
//
HitboxSubtype subType;
String subTypeRaw = hitboxDataRaw.getSubType();
if(subTypeRaw == null){
subTypeRaw = HitboxData.HITBOX_SUBTYPE_REUGLAR;
}
switch(subTypeRaw){
case HitboxData.HITBOX_SUBTYPE_SWEET: {
subType = HitboxSubtype.SWEET;
} break;
case HitboxData.HITBOX_SUBTYPE_REUGLAR: {
subType = HitboxSubtype.REGULAR;
} break;
case HitboxData.HITBOX_SUBTYPE_SOUR: {
subType = HitboxSubtype.SOUR;
} break;
default: {
subType = HitboxSubtype.REGULAR;
} break;
}
if(hitboxDataRaw.getBone() != null){
rVal.hitboxGeomMap.put(hitboxDataRaw.getBone(),geom);
}
rVal.geoms.add(geom);
rVal.geomStateMap.put(geom,new HitboxState(hitboxDataRaw.getBone(), hitboxDataRaw, type, subType, shapeType, false));
}
//create body with all the shapes
DGeom[] geomArray = rVal.geoms.toArray(new DGeom[rVal.geoms.size()]);
rVal.body = CollisionBodyCreation.createBodyWithShapes(manager.getCollisionEngine(), geomArray);
//register collidable with collision engine
Collidable collidable = new Collidable(entity, Collidable.TYPE_OBJECT);
manager.getCollisionEngine().registerCollisionObject(rVal.body, collidable);
//attach
entity.putData(EntityDataStrings.HITBOX_DATA, rVal);
rVal.parent = entity;
//register
manager.registerHitbox(rVal);
rVal.manager = manager;
return rVal;
}
/**
* Create hitbox state for an entity
* @param collisionEngine the collision engine
* @param entity The entity to attach the state to
* @param data The hitbox data to apply
* @param callback The callback that provides a position for the hitbox each frame
* @return The hitbox state that has been attached to the entity
*/
public static HitboxCollectionState attachHitboxStateWithCallback(HitboxManager manager, CollisionEngine collisionEngine, Entity entity, HitboxData data, HitboxPositionCallback callback){
HitboxCollectionState rVal = new HitboxCollectionState();
//create the shapes
rVal.hitboxGeomMap.put(data.getBone(),CollisionBodyCreation.createShapeSphere(collisionEngine, data.getRadius(), Collidable.TYPE_OBJECT_BIT));
//create body with all the shapes
DGeom[] geomArray = rVal.hitboxGeomMap.values().toArray(new DGeom[rVal.hitboxGeomMap.values().size()]);
rVal.body = CollisionBodyCreation.createBodyWithShapes(collisionEngine, geomArray);
//register collidable with collision engine
Collidable collidable = new Collidable(entity, Collidable.TYPE_OBJECT);
collisionEngine.registerCollisionObject(rVal.body, collidable);
//attach
entity.putData(EntityDataStrings.HITBOX_DATA, rVal);
rVal.parent = entity;
//register
manager.registerHitbox(rVal);
rVal.manager = manager;
return rVal;
}
/**
* Clears the collision status of all shapes
*/
public void clearCollisions(){
for(DGeom geom : this.geoms){
HitboxState shapeStatus = this.geomStateMap.get(geom);
shapeStatus.setHadCollision(false);
}
}
/**
* Updates the positions of all hitboxes
*/
public void updateHitboxPositions(CollisionEngine collisionEngine){
if(parent != null && !isServer && EntityUtils.getActor(parent) != null){
if(!this.hitboxGeomMap.isEmpty()){
Vector3d entityPosition = EntityUtils.getPosition(parent);
this.body.setPosition(PhysicsUtils.jomlVecToOdeVec(entityPosition));
for(String boneName : this.hitboxGeomMap.keySet()){
Vector3f bonePosition = EntityUtils.getActor(parent).getBonePosition(boneName);
DGeom geom = this.hitboxGeomMap.get(boneName);
HitboxState shapeStatus = this.geomStateMap.get(geom);
switch(shapeStatus.shapeType){
case SPHERE: {
this.updateSphereShapePosition(collisionEngine,boneName,shapeStatus,bonePosition);
} break;
case CAPSULE: {
this.updateCapsuleShapePosition(collisionEngine,boneName,shapeStatus,bonePosition);
} break;
case STATIC_CAPSULE: {
} break;
}
}
} else if(positionCallback != null){
DGeom geom = body.getGeomIterator().next();
Vector3d worldPosition = this.positionCallback.getPosition();
Quaterniond rotation = new Quaterniond().identity();
PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, worldPosition, rotation);
}
} else if(parent != null && isServer && EntityUtils.getPoseActor(parent) != null){
if(!this.hitboxGeomMap.isEmpty()){
Vector3d entityPosition = EntityUtils.getPosition(parent);
this.body.setPosition(PhysicsUtils.jomlVecToOdeVec(entityPosition));
for(String boneName : this.hitboxGeomMap.keySet()){
Vector3f bonePosition = EntityUtils.getPoseActor(parent).getBonePosition(boneName);
DGeom geom = this.hitboxGeomMap.get(boneName);
HitboxState shapeStatus = this.geomStateMap.get(geom);
switch(shapeStatus.shapeType){
case SPHERE: {
this.updateSphereShapePosition(collisionEngine,boneName,shapeStatus,bonePosition);
} break;
case CAPSULE: {
this.updateCapsuleShapePosition(collisionEngine,boneName,shapeStatus,bonePosition);
} break;
case STATIC_CAPSULE: {
} break;
}
}
} else if(positionCallback != null){
DGeom geom = body.getGeomIterator().next();
Vector3d worldPosition = this.positionCallback.getPosition();
Quaterniond rotation = new Quaterniond().identity();
PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, worldPosition, rotation);
}
} else if(parent != null && isServer){
for(DGeom geom : this.geoms){
HitboxState shapeStatus = this.geomStateMap.get(geom);
switch(shapeStatus.shapeType){
case SPHERE: {
} break;
case CAPSULE: {
} break;
case STATIC_CAPSULE: {
this.updateStaticCapsulePosition(collisionEngine, geom, shapeStatus);
} break;
}
}
}
}
/**
* Updates the position of the geom for a static capsule
* @param collisionEngine The collision engine
* @param boneName The name of the bone the static capsule is attached to
* @param bonePosition The position of the bone
*/
private void updateStaticCapsulePosition(CollisionEngine collisionEngine, DGeom geom, HitboxState shapeStatus){
Vector3d parentPos = EntityUtils.getPosition(parent);
PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, parentPos, new Quaterniond(0.707,0,0,0.707));
}
/**
* Updates the position of a sphere-shape-type hitbox
* @param collisionEngine The collision engine
* @param boneName The name of the bone
* @param bonePosition the position of the bone
*/
private void updateSphereShapePosition(CollisionEngine collisionEngine, String boneName, HitboxState hitboxState, Vector3f bonePosition){
DGeom geom = this.hitboxGeomMap.get(boneName);
//get offset's transform
Vector3d offsetPosition = DataFormatUtil.getDoubleListAsVector(hitboxState.getHitboxData().getOffset());
Quaterniond offsetRotation = new Quaterniond();
//the bone's transform
Vector3d bonePositionD = new Vector3d(bonePosition);
Quaterniond boneRotation = new Quaterniond();
//the parent's transform
Vector3d parentPosition = EntityUtils.getPosition(parent);
Quaterniond parentRotation = EntityUtils.getRotation(parent);
Vector3d parentScale = new Vector3d(EntityUtils.getScale(parent));
//calculate
Vector3d hitboxPos = AttachUtils.calculateBoneAttachmentPosition(offsetPosition, offsetRotation, bonePositionD, boneRotation, parentPosition, parentRotation, parentScale);
PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, hitboxPos, new Quaterniond());
}
/**
* Updates the position of a capsule-shape hitbox
* @param collisionEngine
* @param boneName
* @param bonePosition
*/
private void updateCapsuleShapePosition(CollisionEngine collisionEngine, String boneName, HitboxState hitboxState, Vector3f bonePosition){
//get data about the hitbox
DGeom geom = this.hitboxGeomMap.get(boneName);
Vector3d previousWorldPos = hitboxState.getPreviousWorldPos();
double length = hitboxState.getHitboxData().getRadius();
//get offset's transform
Vector3d offsetPosition = DataFormatUtil.getDoubleListAsVector(hitboxState.getHitboxData().getOffset());
Quaterniond offsetRotation = new Quaterniond();
//the bone's transform
Vector3d bonePositionD = new Vector3d(bonePosition);
Quaterniond boneRotation = new Quaterniond();
//the parent's transform
Vector3d parentPosition = EntityUtils.getPosition(parent);
Quaterniond parentRotation = EntityUtils.getRotation(parent);
Vector3d parentScale = new Vector3d(EntityUtils.getScale(parent));
//calculate
Vector3d worldPosition = AttachUtils.calculateBoneAttachmentPosition(offsetPosition, offsetRotation, bonePositionD, boneRotation, parentPosition, parentRotation, parentScale);
Quaterniond worldRotation = new Quaterniond();
if(previousWorldPos != null){
//called all subsequent updates to hitbox position
//destroy old capsule
this.geomStateMap.remove(geom);
this.geoms.remove(geom);
CollisionBodyCreation.destroyShape(collisionEngine, geom);
//calculate position between new world point and old world point
Vector3d bodyPosition = new Vector3d(worldPosition).lerp(previousWorldPos, 0.5);
//calculate rotation from old position to new position
//the second quaternion is a rotation along the x axis. This is used to put the hitbox rotation into ode's space
//ode is Z-axis-up
if(previousWorldPos.distance(worldPosition) > 0.0){
worldRotation = MathUtils.calculateRotationFromPointToPoint(previousWorldPos,worldPosition).mul(new Quaterniond(0,0.707,0,0.707));
}
//create new capsule
length = previousWorldPos.distance(worldPosition) / 2.0;
if(length > 5000 || Double.isNaN(length) || Double.isInfinite(length) || length < 0){
if(length < 0){
LoggerInterface.loggerEngine.WARNING("Length is too short! " + length);
}
if(length > 5000){
LoggerInterface.loggerEngine.WARNING("Length is too long! " + length);
}
if(Double.isNaN(length) || Double.isInfinite(length)){
LoggerInterface.loggerEngine.WARNING("Length is invalid number! " + length);
}
if(Double.isNaN(previousWorldPos.x) || Double.isInfinite(previousWorldPos.x)){
LoggerInterface.loggerEngine.WARNING("Previous hitbox position isn't valid!");
}
if(Double.isNaN(worldPosition.x) || Double.isInfinite(worldPosition.x)){
LoggerInterface.loggerEngine.WARNING("Current hitbox position isn't valid!");
}
length = 0.1;
}
geom = CollisionBodyCreation.createCapsuleShape(manager.getCollisionEngine(), hitboxState.getHitboxData().getRadius(), length, Collidable.TYPE_OBJECT_BIT);
CollisionBodyCreation.attachGeomToBody(collisionEngine,body,geom);
PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, bodyPosition, worldRotation);
} else {
//called first time the hitbox updates position
this.geomStateMap.remove(geom);
this.geoms.remove(geom);
CollisionBodyCreation.destroyShape(collisionEngine, geom);
//create new capsule
geom = CollisionBodyCreation.createCapsuleShape(manager.getCollisionEngine(), hitboxState.getHitboxData().getRadius(), length, Collidable.TYPE_OBJECT_BIT);
CollisionBodyCreation.attachGeomToBody(collisionEngine,body,geom);
}
//update maps and other variables for next frame
this.hitboxGeomMap.put(boneName,geom);
this.geomStateMap.put(geom,hitboxState);
this.geoms.add(geom);
hitboxState.setPreviousWorldPos(worldPosition);
}
/**
* Gets the status of a shape in the hitbox object
* @param geom The geometry that is the shape within the hitbox data
* @return The status of the shape
*/
public HitboxState getShapeStatus(DGeom geom){
return this.geomStateMap.get(geom);
}
/**
* Gets the hitbox state of the entity
* @param entity the entity
* @return the hitbox state if it exists
*/
public static HitboxCollectionState getHitboxState(Entity entity){
return (HitboxCollectionState)entity.getData(EntityDataStrings.HITBOX_DATA);
}
/**
* Checks whether the entity has hitbox state or not
* @param entity the entity to check
* @return true if there is hitbox state, false otherwise
*/
public static boolean hasHitboxState(Entity entity){
return entity.containsKey(EntityDataStrings.HITBOX_DATA);
}
/**
* Destroys the hitbox state and removes it from the entity
* @param entity the entity
* @return The hitbox state if it exists, null otherwise
*/
public static HitboxCollectionState destroyHitboxState(Entity entity){
HitboxCollectionState state = null;
if(hasHitboxState(entity)){
state = getHitboxState(entity);
state.manager.deregisterHitbox(state);
}
return state;
}
/**
* Gets whether the hitbox state is active or not
* @return true if active, false otherwise
*/
public boolean isActive(){
return active;
}
/**
* Sets the active state of the hitbox
* @param state true to make it active, false otherwise
*/
public void setActive(boolean state){
this.active = state;
for(DGeom geom : this.geoms){
HitboxState shapeState = this.getShapeStatus(geom);
shapeState.setActive(state);
}
}
/**
* Gets the block override status
* @return true if should override hitboxes with blockboxes, false otherwise
*/
public boolean isBlockOverride(){
return this.blockOverride;
}
/**
* Sets the block override of the hitbox
* @param state true to override attack hitboxes with block boxes, false otherwise
*/
public void setBlockOverride(boolean state){
this.blockOverride = state;
for(DGeom geom : this.geoms){
HitboxState shapeState = this.getShapeStatus(geom);
shapeState.setBlockOverride(state);
}
}
/**
* Gets the list of all DGeoms in the data
* @return the list of all DGeoms
*/
public List<DGeom> getGeometries(){
return this.geoms;
}
/**
* Gets the set of bone names in the state data
* @return The set of bone names in the state data
*/
public Set<String> getBones(){
return this.hitboxGeomMap.keySet();
}
/**
* Gets geometry on a single hitbox based on its bone name
* @param boneName the bone name
* @return the hitbox geometry
*/
public DGeom getGeometry(String boneName){
return this.hitboxGeomMap.get(boneName);
}
/**
* The status of a single shape inside the overall hitbox data
* IE a single sphere on the overall body
*/
public static class HitboxState {
/**
* Types of geometry that can be used as individual shapes within a hitbox
*/
public enum HitboxShapeType {
//this is a true sphere. It will teleport every frame to its new position
SPHERE,
//for this one, the shape is a capsule in the collision engine, however
//the capsule is used to have continuity between the last position the hitbox occupied and the current one
CAPSULE,
//this is a true static capsule, it doesn't act as two connected spheres but is instead a capsule that teleports between frames
STATIC_CAPSULE,
}
//the name of the bone the hitbox is attached to
String boneName;
//the type of hitbox
HitboxType type;
//the subtype
HitboxSubtype subType;
//the type of geometry
HitboxShapeType shapeType;
//controls whether the hitbox is active
boolean isActive;
//controls whether the block override is active or not
//if it is active, hitboxes should behave like block boxes
boolean blockOverride = false;
//the previous position of this hitbox shape
Vector3d previousWorldPos = null;
//if true, just had a collision
boolean hadCollision = false;
//the data of the hitbox
HitboxData data;
/**
* Creates a status object for a hitbox
* @param boneName The name of the bone the hitbox is attached to, if any
* @param data the hitbox data object
* @param type The type of hitbox
* @param subType The subtype of hitbox
* @param shapeType The type of shape the hitbox is
* @param isActive if the hitbox is active or not
*/
public HitboxState(String boneName, HitboxData data, HitboxType type, HitboxSubtype subType, HitboxShapeType shapeType, boolean isActive){
this.boneName = boneName;
this.data = data;
this.type = type;
this.subType = subType;
this.shapeType = shapeType;
this.isActive = isActive;
}
/**
* Gets the name of the bone the hitbox is attached to
* @return The name of the bone
*/
public String getBoneName(){
return boneName;
}
/**
* Sets the name of the bone the hitbox is attached to
* @param boneName The bone name
*/
public void setBoneName(String boneName){
this.boneName = boneName;
}
/**
* Gets the hitbox data for this shape
* @return The data
*/
public HitboxData getHitboxData(){
return this.data;
}
/**
* Sets the hitbox data for this shape
* @param data The data
*/
public void setHitboxData(HitboxData data){
this.data = data;
}
/**
* Gets the type of hitbox
* @return The type
*/
public HitboxType getType(){
return type;
}
/**
* Sets the type of hitbox
* @param type The type
*/
public void setType(HitboxType type){
this.type = type;
}
/**
* Gets the subtype of the hitbox
* @return The subtype
*/
public HitboxSubtype getSubType(){
return subType;
}
/**
* Sets the subtype of the hitbox
* @param subType The subtype
*/
public void setSubType(HitboxSubtype subType){
this.subType = subType;
}
/**
* Gets whether the hitbox is active or not
* @return true if active, false otherwise
*/
public boolean isActive(){
return isActive;
}
/**
* Sets whether the hitbox is active or not
* @param active true for active, false otherwise
*/
public void setActive(boolean active){
this.isActive = active;
}
/**
* Gets whether the block override is active or not
* @return true if the block override is active, false otherwise
*/
public boolean isBlockOverride(){
return blockOverride;
}
/**
* Sets whether the block override is active or not
* @param blockOverride true if the block override is active, false otherwise
*/
public void setBlockOverride(boolean blockOverride){
this.blockOverride = blockOverride;
}
/**
* Gets the previous world position of this hitbox
* @return The previous world position
*/
public Vector3d getPreviousWorldPos(){
return this.previousWorldPos;
}
/**
* sets the previous world position of this hitbox shape
* @param previousWorldPos The previous world position
*/
public void setPreviousWorldPos(Vector3d previousWorldPos){
this.previousWorldPos = previousWorldPos;
}
/**
* Sets the status of whether this hitbox just had a collision or not
* @param hadCollision true if had a collision, false otherwise
*/
public void setHadCollision(boolean hadCollision){
this.hadCollision = hadCollision;
}
/**
* Gets the collision status of the hitbox
* @return true if had a collision, false otherwise
*/
public boolean getHadCollision(){
return this.hadCollision;
}
}
}