Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request, Multi type system #528

Open
XANOZOID opened this issue Nov 13, 2017 · 5 comments
Open

Feature Request, Multi type system #528

XANOZOID opened this issue Nov 13, 2017 · 5 comments

Comments

@XANOZOID
Copy link
Member

  • HaxePunk version: Dev Latest
  • Target(s): Cross-Platform

Reason:

Unless I'm mistaken, entities can only have one type. This is perfectly fine and can work with any project, but coming from other engines where being able to easily associate an instance with more than one "group/type" is a real life-improvement. Even something as simple as being able to get elements by inheritance could save some extra overhead on custom project levels.

Request:

My proposal is a feature which enables entities the ability to associate themselves with more than one type.

Applications:

  • easily referring to multiple instances under one name
  • prevent writing meta-data groups, and or group enums, per project basis
  • nullify, in the future, some cases of applying a list of types to check (as oppose to an inherited group)

Potential Suggestions:

My generalized approach, to prevent legacy code from being broken and to reduce end user complexity, will be done by defining an entity's type as an abstract data type. An abstract type that can work with strings, or an array of strings.
Use:

// ... inside of an entitie's `new` function

// normal single type
this.type = "enemy";

// or multiple types, useful for things like inheritance
this.type = ["enemy", "bats"];

Things to keep in mind:

  • Because type will be an abstract type that can be a string or array of strings, I theorize this could be added seamlessly into the code in little to no time.
  • I theorize we could replace the data type of type and _type right now with the abstract as a placeholder for what it could turn out to be
@XANOZOID
Copy link
Member Author

If anyone doesn't have any complaints against this proposal I'd love to give the implementation a go

@bendmorris
Copy link
Member

I like the idea. Would APIs like Entity.collide that currently accept a string parameter also accept arrays? If so, what are the semantics - will it match any of those types, or only all of them together?

Potentially this will trigger an extra array allocation when assigning a string to type, but that seems acceptable to me. Users could cache and reuse type arrays to prevent the extra allocation.

@Kasparsu
Copy link

Kasparsu commented Apr 4, 2018

option would be keep Entity.collide as it is only accepting string and add methods Entity.collideAny and Entity.collideExact that would provide clarification as well as both use cases while not changing original use case.

@XANOZOID
Copy link
Member Author

XANOZOID commented May 23, 2019

Highlights;

  1. Make functions which take strings optionally accept arrays, or don't change it
    - Macros could possibly remove run time checks
    - If it's not changed, Arrays resolve string at index 0 via abstraction
  2. I bring up a problem about using this system for describing type-lineage and how to integrate it with groups
    - I recommend reading the solution

Part 1: How it should effect API

@bendmorris To clarify some things

The argument for multiple types is so that an element can label itself as more than one type. This would allow for accessing all entities which are a part of a group.

I would advise against making any functionality in the engine itself that attempts to find all elements which have 'all included types'. This seems like a good case for the end user to just add a flag for entities belonging to a group of types rather than us querying for entities that have a set of types.

The problem with APIs like Entity.collide is an interesting situation. I prefer to not introduce breaking changes with this added feature and I would like for unnecessary allocations to be avoided if possible. As an immediate implementation, it would make sense to change the signature to accept either a string or an EntityType abstract.

As a possible solution for removing unnecessary allocation and removing the performance loss of runtime type-checking, we could turn the function into a macro which inlines a call to the old entity.collide, when passed a string, otherwise a method which basically loops over entity.collide.

I guess that means it will become an improved version of collideTypes with compile-time benefits.

Part 2: The problem with Lineage VS Groups

Original Intention

My original intention for this "Multi Type" system was actually for describing type-lineage in a way that doesn't require constantly updating the lineage from the root object or some elevated location. It's simple for inheriting instances to track this. That's where my proposal of a "multi-type" system was more akin to entities belonging to multiple groups, and not an idea not related to lineage.

So the real problem, I never considered, was how do we correctly define a relationship that represents type lineage and a group of types seamlessly?

Why lineage doesn't work this way

Because my original intentions were to describe type relations(lineage), and not a group of types, the currently proposed way simply doesn't have a concrete way of defining what the base type is. When it comes to lineage there is a multi-directional relation from any type that is not the root type. However, if you were to use this system for describing an entity for belong to a group of unrelated types - it's usable. My intention was originally to add the ability to retrieve a type and its descending types if any.

So here's exactly the problem that arises when you try solving that problem via just including an inherited type in a group of types.

  • Say A extends B and C extends B
  • If we want to access all 'C' entities, we would probably know this ahead of time and just pass "C"
  • Problem: If we wanted to get all C entities, but we don't have the luxury of statically passing "C" and C's type looks like [C,B,A] (because of inheritance), then all of a sudden we are unintentionally grabbing its parents
    • This does not represent the lineage it's trying to convey. A is not a C, but C is A!
  • But if we wanted to grab all of 'A' entities, we would be able to easily identify every element which classifies under A because A, B, and C have 'A' in their type chain

Solving the problem (Type inheritance with Type Groups)

This problem is tricky because type inheritance and having a group of types are two different functionalities that work together.

I originally theorized two ways of solving this problem, then the first solution just seemed too obvious to suggest an alternative option.

Solution Expands on the original proposition and should integrate seamlessly with HXP as is.

The current proposition enables us to conveniently have an entity associate itself under multiple, unrelated, groups. We can say that an entity's type is an array of strings, plainly put. Or we can abstract it so that it can be either an array of strings or a single string - but that's not the highlight here. We can go one level further and instead of making it an array of strings, we can make it an array of EntityTypes. EntityType will be an abstract which is used as a string.

Macro approach:
If the EntityType is constructed from an array of strings, then a macro will be executed that creates a child-parent relationship between the first entry and the rest and then stores that relationship somewhere during compile time. It would probably construct the relationships as flattened arrays of strings (direct and extended child types) by appending its type-string to its parent's list of children.

This EntityType system will need to abstract a little bit further past constructing relations at compile time by also being able to convert types(strings) to flattened arrays of all their extended children. This will make using the references easier to interface with.

The implementation revolving storing and removing entities by type will most likely need to be updated to account for types actually being arrays of strings. Based on how I imagined the references working, though, anything that's actually retrieving by type should be mostly unchanged.

Naive approach:
Make the type property a 2d array of strings instead of a 1d array of strings and then type relations are left as written instead of computed at compile time.

The rest is more or less the same.

Extra Note:
If we use macros/abstracts for defining types it would be interesting to test out compile-time hashing.

@matrefeytontias
Copy link
Member

Upon further discussion in the Discord, we have come up with the following alternative. It uses enums (and could probably be modified to use abstract enums) to recursively describe an actual SolidType type, and thus allows for type grouping as well as type inheritance, all while being backwards-compatible using an extra abstract for automatic conversion from a single String.

SolidType

SolidType is replaced by an enum in the likes of :

enum SolidType
{
    Base(name:String);
    Inherited(name:String, ancestors:Array<SolidType>);
}

This describes a tree structure given that the user plays nice and doesn't do circular inheritance (there are of course ways to check that this is the case, possibly via macro). This could also theoretically be made into a typed enum with a @:const name:String type parameter, but I haven't given that much thought (and it seems rather dubious).

Type grouping

In addition to this modification, the field Entity.type would change from being of type String to Array<SolidType>. This seems problematic for backwards-compatibility, but here again the use of a wrapper abstract can allow the overloading of assignation and comparison operators, which is mostly what this field is used for. In addition to this, the abstract could provide a simple @:to String function in the case of one-element arrays containing a Base enum constructor (this is where a strictly typed enum would be the most useful).

The usual mechanic of allowing either a SolidType or an array of SolidType remains, much like at the moment SolidType handles single Strings and arrays of String.

Type inheritance

Type inheritance is allowed trivially from the construction of SolidType. Any function that searches for a certain type will match any entity whose type has this type as an ancestor of any degree.

Storage and traversal

As previously mentionned, SolidType is designed with a tree structure, which allows for intuitive traversal. As is currently done, Scene would contain a map with all the types of its entities as keys and lists of entities as values, built the same way but traversed differently. Just like before, an entity is added to a list if its type matches the list's key. However, gathering of all the entities of some type now behaves differently. Instead or simply returning a list depending on its key, we return a concatenation of every list whose key is or inherits from the requested type.

To do so, we use a function that returns whether a certain type inherits from another.

using Lambda;

function typeInherits(from:SolidType, ref:String) : Bool
{
    switch(from)
    {
    case Base(name):
        return name == ref;
    case Inherited(name, ancestors):
        return name == ref || ancestors.exists(x => findType(x, ref));
    }
}

This can be safely used in places where string equality is used, while making sure that for loops aren't broken, but instead return values are accumulated.

Worthy of note, and made obvious by this example, is the fact that types do not need to be addressed using their enum constructor. Namely, all of the various collide functions and fetch by type can still accept a String as a parameter. It is merely during the construction and assignation phase that the enum has to be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants