Skip to content

Commit

Permalink
New queries and commands
Browse files Browse the repository at this point in the history
  • Loading branch information
iacobson committed Aug 21, 2023
1 parent 3605898 commit 7a95e0b
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 5 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v0.3.0 (2023-08-21)

### Features

- add a new query `Ecspanse.Query.list_tags/1` to list a component's tags.
- add a new query `Ecspanse.Query.list_components/1` to list all components of an entity.
- add a new command `Ecspanse.Command.clone_entity/1` to clone an entity without its relationships.
- add a new command `Ecspanse.Command.deep_clone_entity/1` to clone an entity with its descendants.

## v0.2.1 (2023-08-20)

### Fixes
Expand All @@ -16,7 +25,7 @@
### Features

- introducing `Ecspanse.Template.Component` and `Ecspanse.Template.Event` to simplify the creation of related components and events.
- add a new function `Ecspanse.Query.fetch_component/2` to fetch a system's component by a list of tags.
- add a new query `Ecspanse.Query.fetch_component/2` to fetch a system's component by a list of tags.

## v0.1.2 (2023-08-14)

Expand Down
69 changes: 66 additions & 3 deletions lib/ecspanse/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,65 @@ defmodule Ecspanse.Command do
|> despawn_entities!()
end

@doc """
Clones the specified entity and returns a new entity with the same components.
Due to the potentially large number of components that may be affected by this operation,
it is recommended to run this function in a synchronous system (such as a `frame_start` or `frame_end` system)
to avoid the need to lock all involved components.
> #### Note {: .info}
>
> The entity's `Ecspanse.Component.Children` and `Ecspanse.Component.Parents` components are not cloned.
> Use `deep_clone_entity!/1` to clone the entity and all of its descendants.
## Examples
```elixir
%Ecspanse.Entity{} = entity = Ecspanse.Command.clone_entity!(compass_entity)
```
"""
@doc group: :entities
@spec clone_entity!(Entity.t()) :: Entity.t()
def clone_entity!(entity) do
components = Ecspanse.Query.list_components(entity)

component_specs =
Enum.map(components, fn component ->
state = component |> Map.from_struct() |> Map.delete(:__meta__) |> Map.to_list()
{component.__struct__, state, Ecspanse.Query.list_tags(component)}
end)

spawn_entity!({Ecspanse.Entity, components: component_specs, children: [], parents: []})
end

@doc """
Clones the specified entity and all of its descendants and returns the newly cloned entity.
Due to the potentially large number of components that may be affected by this operation,
it is recommended to run this function in a synchronous system (such as a `frame_start` or `frame_end` system)
to avoid the need to lock all involved components.
## Examples
```elixir
%Ecspanse.Entity{} = entity = Ecspanse.Command.deep_clone_entity!(enemy_entity)
```
"""
@doc group: :entities
@spec deep_clone_entity!(Entity.t()) :: Entity.t()
def deep_clone_entity!(entity) do
cloned_entity = clone_entity!(entity)
children = Ecspanse.Query.list_children(entity)

case children do
[] ->
cloned_entity

children ->
add_children!([{cloned_entity, Enum.map(children, &deep_clone_entity!/1)}])
cloned_entity
end
end

@doc """
Adds a new component to the specified entity.
Expand Down Expand Up @@ -612,12 +671,16 @@ defmodule Ecspanse.Command do
{module, _, _} when is_atom(module) -> module
end)

children_entities = Keyword.get(opts, :children, [])
parents_entities = Keyword.get(opts, :parents, [])
# allow creation of entities only with empty children and parents
children_entities = Keyword.get(opts, :children)
parents_entities = Keyword.get(opts, :parents)

:ok =
validate_required_opts(operation, component_specs, children_entities, parents_entities)

children_entities = Keyword.get(opts, :children, [])
parents_entities = Keyword.get(opts, :parents, [])

%{
entity: Util.build_entity(entity_id),
component_specs: component_specs,
Expand Down Expand Up @@ -1435,7 +1498,7 @@ defmodule Ecspanse.Command do

# Component CRUD Validations

defp validate_required_opts(operation, [], [], []) do
defp validate_required_opts(operation, [], nil, nil) do
raise Error,
{operation,
"Expected at least one of the following options in the entity_spec when creating an entity: `components`, `children`, `parents`"}
Expand Down
53 changes: 53 additions & 0 deletions lib/ecspanse/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,22 @@ defmodule Ecspanse.Query do
end
end

@doc """
Lists a component's tags.
## Examples
```elixir
[:resource, :available] = Ecspanse.Query.list_tags(gold_component)
```
"""
@doc group: :tags
@spec list_tags(components_state :: struct()) :: list(tag :: atom())
def list_tags(component) do
:ok = validate_components([component])
component.__meta__.tags
end

@doc """
Returns a list of components tagged with a list of tags for all entities.
The components need to be tagged with all the given tags to return.
Expand Down Expand Up @@ -551,6 +567,43 @@ defmodule Ecspanse.Query do
end
end

@doc """
Lists all the components for a given entity.
The output is an unordered list of all the entity's components.
> #### Note {: .info}
>
> The `Ecspanse.Component.Children` and `Ecspanse.Component.Parents`
> components are **excluded** from the output.
>
> Use the provided `list_children/1` and `list_parents/1` functions to
> query the entity's relations.
## Examples
```elixir
[gold_component, gems_component, position_component, energy_component] =
Ecspanse.Query.list_components(hero_entity)
```
"""
@doc group: :components
@spec list_components(Ecspanse.Entity.t()) :: list(components_state :: struct())
def list_components(%Entity{id: id}) do
table = Util.components_state_ets_table()

f =
Ex2ms.fun do
{{entity_id, component_module}, _component_tags, component_state}
when entity_id == ^id and
component_module != Ecspanse.Component.Children and
component_module != Ecspanse.Component.Parents ->
component_state
end

:ets.select(table, f)
end

@doc """
Returns `true` if the entity has a component with the given module.
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Ecspanse.MixProject do
use Mix.Project

@version "0.2.1"
@version "0.3.0"
@source_url "https://github.com/iacobson/ecspanse"

def project do
Expand Down
63 changes: 63 additions & 0 deletions test/ecspanse/command_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,69 @@ defmodule Ecspanse.CommandTest do
end
end

describe "clone_entity!/1" do
test "create a clone of the entity" do
entity =
Ecspanse.Command.spawn_entity!(
{Ecspanse.Entity,
components: [
{TestComponent1, [value: :bar], [:tag_1, :tag_2]},
TestComponent2,
TestComponent3
]}
)

parent_entity = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, children: [entity]})

clone = Ecspanse.Command.clone_entity!(entity)

entity_components = Ecspanse.Query.list_components(entity)
clone_components = Ecspanse.Query.list_components(clone)

assert [parent_entity] == Ecspanse.Query.list_parents(entity)
# relationships are not cloned
assert [] = Ecspanse.Query.list_parents(clone)

entity_component_modules = entity_components |> Enum.map(& &1.__struct__)
cloned_entity_component_modules = clone_components |> Enum.map(& &1.__struct__)

assert [] == entity_component_modules -- cloned_entity_component_modules

{:ok, entity_component_1} = TestComponent1.fetch(entity)
{:ok, cloned_entity_component_1} = TestComponent1.fetch(clone)

assert entity_component_1.value == cloned_entity_component_1.value

assert Ecspanse.Query.list_tags(entity_component_1) ==
Ecspanse.Query.list_tags(cloned_entity_component_1)
end
end

describe "deep_clone_entity/1" do
test "create a clone of the entity and all its descendants" do
assert %Ecspanse.Entity{} =
root_entity =
Ecspanse.Command.spawn_entity!({Ecspanse.Entity, components: [TestComponent1]})

entity_1 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, parents: [root_entity]})
entity_2 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, parents: [entity_1]})
_entity_3 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, parents: [entity_2]})
_entity_4 = Ecspanse.Command.spawn_entity!({Ecspanse.Entity, parents: [root_entity]})

cloned_entity = Ecspanse.Command.deep_clone_entity!(root_entity)

root_entity_children = Ecspanse.Query.list_children(root_entity)
cloned_entity_children = Ecspanse.Query.list_children(cloned_entity)

assert length(root_entity_children) == length(cloned_entity_children)

root_entity_descendants = Ecspanse.Query.list_descendants(root_entity)
cloned_entity_descendants = Ecspanse.Query.list_descendants(cloned_entity)

assert length(root_entity_descendants) == length(cloned_entity_descendants)
end
end

describe "add_components!/1" do
test "adds components to an existing entity" do
assert %Ecspanse.Entity{} =
Expand Down
33 changes: 33 additions & 0 deletions test/ecspanse/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,22 @@ defmodule Ecspanse.QueryTest do
end
end

describe "list_tags" do
test "returns the tags for a component" do
entity =
Ecspanse.Command.spawn_entity!(
{Ecspanse.Entity,
components: [
{TestComponent4, [], [:baz]}
]}
)

{:ok, component} = TestComponent4.fetch(entity)

assert [:foo, :bar, :baz] == Ecspanse.Query.list_tags(component)
end
end

describe "list_tagged_components/1" do
test "returns the components for a list of tags" do
Ecspanse.Command.spawn_entity!(
Expand Down Expand Up @@ -715,6 +731,23 @@ defmodule Ecspanse.QueryTest do
end
end

describe "list_components/1" do
test "returns a list with all entity's components" do
component_modules = [TestComponent1, TestComponent2, TestComponent4, TestComponent5]

entity =
Ecspanse.Command.spawn_entity!({Ecspanse.Entity, components: component_modules})

components = Ecspanse.Query.list_components(entity)

assert length(components) == length(component_modules)

for component <- components do
assert component.__struct__ in component_modules
end
end
end

describe "has_component?/2" do
test "checks if an entity has a certain component" do
entity_1 =
Expand Down
1 change: 1 addition & 0 deletions test/ecspanse_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ defmodule EcspanseTest do
Ecspanse.event(TetsEvent1, batch_key: batch_key)
Ecspanse.event(TetsEvent1, batch_key: batch_key)
Ecspanse.event(TetsEvent2, batch_key: batch_key)
:timer.sleep(100)

assert_receive {:next_frame, state}

Expand Down

0 comments on commit 7a95e0b

Please sign in to comment.