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!