This has plagued me for eons. I'm not even sure which exact terms to use to describe this issue in its most simple form.
Basically:
class ComponentA: Component {}
class ComponentB: Component {}
class ComponentC: Component {}
// func findComponent <ComponentType: Component> (ofType componentClass: ComponentType.Type,
// in entity: [Component]) -> ComponentType?
// ...
// Correctly returns componentA
findComponent(ofType: ComponentA.self, in: [componentA, componentB, componentC])
// Correctly fails to find componentC
findComponent(ofType: ComponentC.self, in: [componentA, componentB])
// Correctly returns componentB
let componentsToFind = [ComponentB.self]
findComponent(ofType: componentsToFind[0], in: [componentA, componentB, componentC])
// Incorrectly returns componentA because it now matches it with `Component` (the superclass)
let componentsToFind2 = [ComponentB.self, ComponentC.self]
findComponent(ofType: componentsToFind2[0], in: [componentA, componentB, componentC])
For the full example, please see this gist and try it in an Xcode Playground:
https://gist.github.com/ShinryakuTako/9e9b6986eff7d5b38e28a19b305e4673
In coComponent
, ComponentType
is the generic type known at compile time, while componentClass
is a type at runtime which could be a subtype of ComponentType
. What you really want is to search for RelayComponent<componentClass>.self
, but I don't think there's a way to do that in Swift without a giant switch statement over all possible subclasses of the class, though maybe someone else knows of a way.
The issue can be simplified as this:
func printInfo<C: Component>(c: C.Type) {
print("\(C.self) vs \(c)")
}
printInfo(c: TestComponentA.self as Component.Type)
This will print Component vs TestComponentA
, while your code assumed that it would be TestComponentA vs TestComponentA
Here's an alternate way to do things that fixes it:
Add two computed properties to Component
(you may want to use different names):
var componentType: Component.Type? { return type(of: self) }
var baseComponent: Component? { return self }
Override those methods in RelayComponent
:
override var componentType: Component.Type? { return target?.componentType }
override var baseComponent: Component? { return target?.baseComponent }
Use those instead of type(of:)
in component(ofType:)
:
func component <ComponentType> (ofType componentClass: ComponentType.Type) -> ComponentType?
where ComponentType : Component
{
return components.first { component in
return (component.componentType == componentClass) // (component is ComponentType)
}?.baseComponent as? ComponentType
}
(You no longer need the explicit check for RelayComponent in coComponent
, relay components will now be found and unwrapped by component(ofType:)
)
1 Like
Thanks!
Ah but this is a little tricky, because I cannot change the implementation of component(ofType:)
as it's an Apple API (from GameplayKit.GKEntity
)
Overriding declarations in extensions is not supported
Overriding non-open instance method outside of its defining module
Update: Fixed!
I modified another method that I had added to GKEntity
, componentOrRelay(ofType:)
.
Although it's not a 100% fix because it doesn't apply to the base GKEntity.component(ofType:)
, it's good enough for my purposes. :)
Thanks again!