Closed mysterymagination closed 1 year ago
I added other collision shapes to the arms, head, torso, and legs. I think with collision channel fiddling that should allow for hit and hurtboxes (with extra damage for headshots or shots in the eyes or something). Maybe even something to shut down his arms and/or legs by targeting them.
todo: how do I get the hitboxes and hurtboxes to move with his animations? They are children of his mesh, but does the animation actually count as a mesh transform? Even if it does, there's currently no association between e.g. specifically the right arm section of the mesh and the right arm hitbox; maybe sockets or bone associations for the collision shapes need to be employed? EDIT: yep, just needed to assign bones in the parent socket fields of the collision shapes
todo: add collision channels to allow for hurtboxes (treant takes damage when these are hit by an attack), hitboxes (player takes damage when these connect, at last during an attack action/animation), and both (e.g. arms can both inflict and receive damage)
one way I've seen to do this, without collision channels, is to add logic to the projectiles to check if the name of the object they collide with contains a certain string e.g. 'arm', 'head' etc. This could work, but would require me to make the hitboxes into BPs with their own event graph so they can check in turn if they have collided with various hurtboxes of the player. Seems like a naive workaround to avoid using collision channels to automatically achieve the same thing.
So I've gone a bit overboard with the anatomy arch and levels of abstraction for defining anatomical form and function... but anyway we're getting somewhat close to being able to place a UArmCapsuleComponent on the treant and then be able to hook up that component's oncomponenthit event to I guess the ProcessHit() fn of the UArmCapsuleComponent::GetAnatomyUnit() return value? That's all well and good, but the API under the hood is a bit fucked -- we can cast the hitting component to IAttacker and the hit component to IDefender and work through those interfaces (although in the case of ASnowball, we implemented IAttacker on the AActor rather than a component so we'd need to somehow know to use the hitting component's owner AActor in that case...), but the IAttacker interface fn CalculateDamageTx calls for an attack name and an attacking AActor, neither of which are easily available. We could look up the attacking AActor probably through the UPrimitiveComponent API but we ideally wouldn't and we certainly don't know anything about the attack name (and shouldn't) when we're processing an incoming hit.
Problems:
So assuming the collision works by sending the collision event to both parties involved with the roles swapped (the event a given registered party gets is the one in which their component is the striking component), what we could do is:
have a top level Actor implement the onhit handler
handler looks up the UAnatomyUnit owned by the striker component and casts that to IAttacker to calc tx damage
TODO: what about IAttackers who aren't implementers of IAnatomy, e.g. ASnowball? Maybe we should move the IAttacker interface up to the UPrimitiveComponent subclass level so we can be assured that we'll have an IAttacker and/or IDefender in a collision event, and then it will be up to the impl to look up anatomy or whatever. Arguably mucks up my form and function partition but not a lot and we have to work within the UE4 API which just gives us UPrimitiveComponents and maybe an owning AActor... Also doesn't super work with Snowball, but that gal already knows how to handle herself.
handler for anatomy collision components looks up the UAnatomyUnit owned by the stricken component and feeds the tx damage in to its calc rx damage function, which takes resistances etc. into account and calls ApplyPointDamage().
todo: add attack animation based on behavior tree state
todo: wtf can't I use a TPair in IDefender?! I keep getting the dreaded Error: Unrecognized type 'TPair' - type must be a UCLASS, USTRUCT, UENUM, or global delegate.
from UHT. I guess if I can't use that for no reason, then I should change the API so it accepts a damagetype as input and it's up to the impl to have a mapping or something that returns the scaling factor for the given damagetype EDIT: switched over to an input damage type paradigm, which makes more sense anyway. TBD why I couldn't use TPair...
todo: extracting from above, create Weapon and Armor classes that can be IAttacker and IDefenders so that we can effectively equip common weapons e.g. a claw to different primitive constructs like the treant's hands and feet. This way we can have common logic that just gets modified by params despite having to have different PrimitiveComponent subclasses.
Well most of the horrendous build errors I introduced with the refactor are sorted now... except I'm getting a bizarre linker error from anyone implementing IAttacker:
[2/3] Link UnrealEditor-Ryddelmyst.dylib
Undefined symbols for architecture x86_64:
"vtable for UExtremityUnit", referenced from:
void InternalConstructor<UExtremityUnit>(FObjectInitializer const&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
UExtremityUnit::UExtremityUnit(FVTableHelper&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
UExtremityUnit::UExtremityUnit(FVTableHelper&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
UExtremityUnit::__VTableCtorCaller(FVTableHelper&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.
"vtable for UClawCapsuleComponent", referenced from:
UClawCapsuleComponent::UClawCapsuleComponent() in Module.Ryddelmyst.cpp.o
UClawCapsuleComponent::UClawCapsuleComponent(FVTableHelper&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
UClawCapsuleComponent::UClawCapsuleComponent(FVTableHelper&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
UClawCapsuleComponent::__VTableCtorCaller(FVTableHelper&) in Module.Ryddelmyst.gen.1_of_3.cpp.o
NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
From what I can tell the IAttacker functions are all (both) implemented, so I don't know what the problem could be. I checked the generated code for the UClawCapsuleComponent class with a missing vtable and the generated code for IAttacker, and it looks ok AFAICS (basically I was just looking for the functions to be the new ones rather than the old ones for that interface, which they were. The rest was generated moonspeak.)
EDIT: I tried removing all the blueprint metadata crap and _Implementation rot from the equation and now things are fine. So the problem is somehow coming from that idiot dance they make you do on https://docs.unrealengine.com/5.0/en-US/interfaces-in-unreal-engine/
EDIT2: aHA!!! Turns out this crazy issue was caused by me duplicating the UFUNCTION metadata in both the interface definition and the implementation function declaration; you need the _Implementation fellas to not have a UFUNCTION macro above them at all apparently.
Hmm, snowballs are not running their OnComponentHit delegate function EDIT: huh turns out you need a function to be decorated with a UFUNCTION() macro for it to work as a function pointer in a function delegate
I added allowprivateaccess=true to the meta tag of armor and exunit props in ExtremityCapsuleComponent and for some reason now they are assignable in-editor, although without the pretty asset preview picture box... whatevs.
For future me, here is the tagging I used: For the UExtremityUnit class: UCLASS(EditInlineNew, Blueprintable, BlueprintType, meta = (DisplayName = "Extremity")) For the ExUnit and Armor properties: UPROPERTY(EditAnywhere, Instanced, BlueprintReadWrite, Category = "Combat", meta = (AllowPrivateAccess = "true")) and the props are TObjectPtr if that matters.
todo: since the choice of attack event and the attack landing event are separate and async, we can't simply have IAttacker::ExecuteAttack() impls call Weapon::AttackMap["something"]->Attack::OnHit(). We need at least two stages, one in which the attack anim is kicked off and any other immediate effects happen, and then a second where a collision with the attack's associated physicality happens that then calls its OnHit.
todo: bridge anatomy and armor such that armor knows what anatomy is under it; I want to have the armor on the head's impl of calcdmgrx multiply damage by a scaling factor, but I don't currently have any way for the armor function impls to know what anatomy is under them.
todo: implement WoodArmor
todo: add UPrimitive subclasses with IDefender and IAnatomy impls to the Maya character
todo: for some reason I'm not seeing Monster::OnHit() running for Maya's BodyCapsuleHitBox component, only for her CollisionCylinder
todo: maybe just put a pin in this shit for now; maybe it's a UE5 bug? Anyway, we can technically get along with what we have for the gameplay we want vis a vis multi-part natural weapon monster's attacks collide with the player's root cylinder and the player's projectiles collide with the monster's multi-part anatomy. The only thing we couldn't do is enable Maya's might high kicks to hit monster bits or for monsters to in-fight smashing bits upon other bits... but that's not really required for the main gameplay I had in mind.
todo: create a subclass of UActorComponent that implements the common OnHit handling of the Clash API
todo: calling ProcessCosts() in Cast() isn't always right because Cast() is not cast like cast a spell, but rather it's cast like cast a stone into a lake a.k.a. throw something. Some metamagic modifies spells so that we cast more than one bullet AActor per spell instance, and we should only be incurring the spell cost once, not per-bullet. It would be handy for this and for looking up metadata like spell casting cost and weighing against character MP before we go ahead with manifesting the spell to have a wrapper Spell class that contains the Snowball AActor and all its children plus some basic per-spell instance data and behavior like casting costs and processing those costs. The Attack object probably should stick to the Clash API stuff of delivering calculated damage to a defender etc. and leave the bookkeeping to something more abstract at a higher level.
todo: ideally the ProcessCosts() function would take a callback listener (could be bolted on to BattleStatsBearer interface) that would inform e.g. RyddelmystCharacter that her MP stat had just changed by N so she can respond by updating her UI. EDIT: I just hooked up the UI to the battle stats
todo: Snowball Clash API impl is almost complete, but the pipeline is broke somewhere because the Snowball AActors are not getting destroyed after collision, which should happen unconditionally in SnowballAttack::OnHit_Implementation(). EDIT: forgot to decorate OnHit in HitBoxerComponent with UFUNCTION, which causes it to silently fail as a ufunction delegate.
todo: our UAttack::OnHit_Implementation() boilerplate code assumes the striking component's owner AActor is also a BattleStatsBearer, which is not the case in e.g. projectiles like Snowballs. For Snowball, we save a handle to the spellcaster that launched it, and that handle should be used as the BattleStatsBearer. How do we want to address this?
todo: add impls of GetBattler() for applicable IAttacker and IDefender implementors, such as ClawCapsuleComponent and BP_ReaperTreantWoodArmor. TBD if we want/can add meaningful impls directly to the CPP or if it should be a BP thing.
todo: wire up HitBoxerComponent in BP_ReaperTreant
todo: now that Snowball implements the Clash API, what happens if we remove simulate physics on it?
todo: nevermind the query collision between my custom uprims; we only actually need snowballs (a physics actor) colliding with the treant's multiple hitboxes and the treant attacks to register against one thing (default collision cylinder) on Maya. So, we need to wire up the Clash API to Maya's collision cylinder and we should be good enough to go
For this we'll need separate hit boxes on the arms/hands or something; the single capsulecomponent around the treant isn't good enough.