BabylonJS / Editor

Community managed visual editor for Babylon.js
http://editor.babylonjs.com/
816 stars 232 forks source link

Why only one script per entity? #335

Open ernest4 opened 2 years ago

ernest4 commented 2 years ago

Is there any particular reason why only one script can be attached to entity? :)

This seems quite limiting as it reduces code reuse. Scripts should represent a custom component that default engine doesn't offer. So I should be able to attach multiple such custom components to an Entity.

e.g. Unity allows this

Screenshot 2021-12-02 at 22 54 30

Allowing this would be a step towards supporting ECS. Once you can attach N arbitrary custom components to an Entity, possibility opens to start making "system" scripts that will query for and iterate over those entities (like the Unity DOTS)

julien-moreau commented 2 years ago

Hi @ernest4 ! Welcome back :)

This is actually in my todolist. Right now I'm focusing on the "Assets Browser" panel that will be available in a near future (v4.1.0). I'm keeping this issue opened as an enhancement so you'll get notified on each update.

As you said in your email (will answer in few minutes), don't hesitate to have a look to the steps to add the support of multiple script components :)

Lozamded commented 1 year ago

Some others engines like godot have one script per entity, so is not to strange to me

Ryslan342 commented 1 year ago

Can I check the current status?

I'm also interested in the possibility of attaching multiple scripts to one object. It would also be cool to be able to manage the scripts attached to the entity from the code, at the end I will give an example.

I also understand the problem that in the current form this is not possible. Right now the script is implemented through inheritance, and JS doesn't support multiple inheritance.

Can do it in style (I will write my vision in the abstract, at the level of interfaces.)

/*
* This is an abstract script class.
* The user will inherit from this class, or implement its interface. I don't know which is better.
* */
@EditorRegister() // I propose to add the ability for the programmer to control whether scripts should be displayed in the editor
abstract class ScriptComponent<TEntity extends Node> {
  private constructor(protected entity: TEntity) {};

  onStart() {
    // Redefine this function after extending
  }

  onUpdate() {
    // Redefine this function after extending
  }

  onRemove() {
    // Will be called when an object is deleted or when a script is detached
    // Redefine this function after extending
  }

  killScript();
}

/*
* Just a constructor type for convenience. This type means that we are passing the class directly, not an instance of it.
* */
type ScriptComponentClass<TEntity extends Node> = new (entity: TEntity) => ScriptComponent<TEntity>;

/*
* Each node has its own script manager.
* Or the node can implement such functionality itself, it will even simplify the binding of events and context.
*
* The main essence is the management of current scripts that were "attached" to the node.
* Attaching new scripts, deleting, forwarding events.
* */
interface ScriptsManager<TEntity extends Node> {
  // scripts: ScriptComponent<TEntity>[];

  // Get attached script component by constructor class. "instanceof" can help you with it
  get(constructor: ScriptComponentClass<TEntity>): ScriptComponent<TEntity> | null;

  getList(script): ScriptComponent<TEntity>[];

  find(searchFunc: (script: ScriptComponent<TEntity>) => boolean): ScriptComponent<TEntity>[];
  findOne(searchFunc: (script: ScriptComponent<TEntity>) => boolean): ScriptComponent<TEntity>;

  attach(constructor: ScriptComponentClass<TEntity>, scriptParam: Map<string, any>): ScriptComponent<TEntity>;

  detach(script: ScriptComponent<TEntity>);
  detach(script: ScriptComponent<TEntity>[]);
  detach(searchFunc: (script: ScriptComponent<TEntity>) => boolean);
}

/*
* Each node has a script manager
* */
interface Node {
  scripts: ScriptsManager<Node>;
}
interface Mesh {
  scripts: ScriptsManager<Mesh>;
}

And example of usage

// A normal enemy that attacks the player. It has no other tasks, it is used everywhere.
class AggressiveEnemy extends ScriptComponent<Mesh> {
  // @visibleInInspector("number", "Walk Speed", 1) -It's old version, but i think "scriptParam" will be more actual
  @scriptParam("number", "runSpeed" /* is key */, { title: "Enemy running speed", description: "Long text, show it on cursor hover", default: 1 })
  private _runSpeed: number;

  @scriptParam("number", "damage" /* is key */, { title: "Damage", default: 1 })
  private _damage: number;

  onUpdate() {
    // go to user and attack him
  }
}

// this script wait any interaction with player. How example
class HiddenEnemy extends ScriptComponent<Mesh>{

  onUpdate() {
    if (/* distance to user <= 5m */) {
      this.onPlayerFoundThisObject();
    }
  }

  onPlayerFoundThisObject() {
    // Play animation as the enemy climbs out of cover
    this.display();

    // Now it mesh is normal enemy
    this.entity.scripts.attach(AggressiveEnemy, {
      runSpeed: 1,
      damage: 5
      // ............
    });

    // remove this script
    this.entity.scripts.detach(this);
  }
}
Ryslan342 commented 1 year ago

This is just an opinion based on how I've seen the implementation elsewhere. And I like this implementation, for me, it will be convenient.

Can you tell me if you have a public discussion place where can I offer and discuss? I wouldn't mind discussions and other ideas!

I'm new to the Babylon community, and I still don't know how everything works for you, I'll be glad if you answer)

Ryslan342 commented 1 year ago

It looks like the "Behaviors" system already implements similar functionality, and it can be used similarly! My version above is essentially a duplication of functionality, I think updating the Node class is superfluous.

For such logic, you only need to update the Editor, how it uses the classes and how it attaches them. I plan to do this within a week, I am 100% sure that I know how to solve my need, in my project.

If I succeed, I will try to bring it to a good view. Add backward compatibility and create a PR