diff --git a/guides/scaffolds_guide.md b/guides/scaffolds_guide.md new file mode 100644 index 0000000..bbebe2d --- /dev/null +++ b/guides/scaffolds_guide.md @@ -0,0 +1,49 @@ +# Scaffold Guides + +Some component and system pairs are often developed together. + +A good example of these pairs are position components and a movement system. In a 2D space, you will have an `XPosition`, `YPosition`,`Xvelocity` and `YVelocity` component and a movement system that updates these components. Other examples would be health/damage systems, where your entity has health/damage point components and a health system updates their values. + +By using a scaffold, you will now be able to generate these component system pairs. For instance; + +``` +mix ecsx.gen.scaffold movement2d +``` + +would generate these components and systems for movement on a 2d plane. + + +## Existing scaffolds +### Movement2d +``` +mix ecsx.gen.scaffold movement2d +``` + +- This creates 4 components, all with the `float` data type +``` + - x_position + - y_position + - x_velocity + - y_velocity +``` + +and a single system, `movement2d` + +The movement system will come with 2 private functions, `move_x` and `move_y` that can be used in the run function to update the entitiy's position. + +### Health +``` +mix ecsx.gen.scaffold health +``` + +This creates a `health_points` component, with an `integer` data type and a single system, `health` + +The health system has 2 private functions, `take_damage/2` and `add_health/2` which can be used to reduce health and increase health of an entity. + +## Scaffold Systems in Progress +### Collision +``` +mix ecsx.gen.scaffold collision +``` + +These allow collisions to be added to entities. It also implements the collision force calculations for you. \ No newline at end of file diff --git a/lib/mix/tasks/ecsx.gen.scaffold.ex b/lib/mix/tasks/ecsx.gen.scaffold.ex new file mode 100644 index 0000000..35cb966 --- /dev/null +++ b/lib/mix/tasks/ecsx.gen.scaffold.ex @@ -0,0 +1,79 @@ +defmodule Mix.Tasks.Ecsx.Gen.Scaffold do + @shortdoc "Generates a scaffold for common system and component combinations like movement systems" + + @moduledoc """ + Scaffolds a system and component pair for an ECSx appliaction + + $ mix ecxs.gen.scaffold movement2d + + The argument is the type of scaffold to be generated + + Valid scaffolds are: + * movement2d + * health + * collision + """ + + use Mix.Task + + alias Mix.Tasks.Ecsx.Gen.System + alias Mix.Tasks.Ecsx.Gen.Component + + @valid_scaffold_types ~w(movement2d health collision) + + @doc false + def run([]) do + "Invalid arguments - must provide a valid scaffold type" + |> message_with_help() + |> Mix.raise() + end + + def run([scaffold_type | _opts] = _args) do + scaffold_type = validate(scaffold_type) + # {opts, _, _} = OptionParser.parse(opts, strict: [namespace: :string]) + create_scaffold(scaffold_type) + end + + defp message_with_help(message) do + """ + #{message} + + mix ecsx.gen.sacffold expects a scaffold type. + + For example: + + mix ecsx.gen.scaffold movement2d + + """ + end + + defp validate(scaffold_type) when scaffold_type in @valid_scaffold_types, + do: String.to_atom(scaffold_type) + + defp validate(_), + do: + Mix.raise( + "Invalid scaffold type. Possible scaffold types are: #{inspect(@valid_scaffold_types)}}" + ) + + defp create_scaffold(scaffold_type) do + create_associated_scaffold_system(scaffold_type) + create_associated_scaffold_components(scaffold_type) + end + + # create systems + defp create_associated_scaffold_system(:movement2d), do: System.run(["Movement2d"]) + + defp create_associated_scaffold_system(:health), do: System.run(["Health"]) + + # create components + defp create_associated_scaffold_components(:movement2d) do + ["XPosition", "YPosition", "XVelocity", "YVelocity"] + |> Enum.each(fn component -> Component.run([component, "float"]) end) + end + + defp create_associated_scaffold_components(:health) do + ["HealthPoints", "DamagePoints"] + |> Enum.each(fn component -> Component.run([component, "integer"]) end) + end +end diff --git a/lib/mix/tasks/ecsx.gen.system.ex b/lib/mix/tasks/ecsx.gen.system.ex index da06dd7..2fcc972 100644 --- a/lib/mix/tasks/ecsx.gen.system.ex +++ b/lib/mix/tasks/ecsx.gen.system.ex @@ -35,7 +35,19 @@ defmodule Mix.Tasks.Ecsx.Gen.System do defp create_system_file(system_name) do filename = Macro.underscore(system_name) target = "lib/#{Helpers.otp_app()}/systems/#{filename}.ex" - source = Application.app_dir(:ecsx, "/priv/templates/system.ex") + + source = + case system_name do + "Movement2d" -> + Application.app_dir(:ecsx, "/priv/templates/scaffold_templates/movement2d.ex") + + "Health" -> + Application.app_dir(:ecsx, "/priv/templates/scaffold_templates/health.ex") + + _ -> + Application.app_dir(:ecsx, "/priv/templates/system.ex") + end + binding = [app_name: Helpers.root_module(), system_name: system_name] Mix.Generator.create_file(target, EEx.eval_file(source, binding)) diff --git a/mix.lock b/mix.lock index cf7c6cb..f6426c0 100644 --- a/mix.lock +++ b/mix.lock @@ -19,7 +19,7 @@ "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, diff --git a/priv/templates/scaffold_templates/health.ex b/priv/templates/scaffold_templates/health.ex new file mode 100644 index 0000000..d14d804 --- /dev/null +++ b/priv/templates/scaffold_templates/health.ex @@ -0,0 +1,24 @@ +defmodule <%= app_name %>.Systems.Health do + @moduledoc """ + Documentation for <%= system_name %> system. + """ + @behaviour ECSx.System + + @impl ECSx.System + def run do + # System logic + :ok + end + + # These functions can be used to update the health of entities. + + defp take_damage(entity, damage_points) do + health = <%= app_name %>.Components.HealthPoints.get(entity) + <%= app_name %>.Components.HealthPoints.update(health - damage_points) + end + + defp add_health(entity, extra_health_points) do + health = <%= app_name %>.Components.HealthPoints.get(entity) + <%= app_name %>.Components.HealthPoints.update(health + extra_health_points) + end +end diff --git a/priv/templates/scaffold_templates/movement2d.ex b/priv/templates/scaffold_templates/movement2d.ex new file mode 100644 index 0000000..0e1cc7f --- /dev/null +++ b/priv/templates/scaffold_templates/movement2d.ex @@ -0,0 +1,26 @@ +defmodule <%= app_name %>.Systems.Movement2d do + @moduledoc """ + Documentation for <%= system_name %> system. + """ + @behaviour ECSx.System + + @impl ECSx.System + def run do + # System logic + :ok + end + + # These functions can be used to update the position of entities. + + defp move_x(entity) do + x_pos = <%= app_name %>.Components.XPosition.get(entity) + x_velocity = <%= app_name %>.Components.XVelocity.get(entity) + <%= app_name %>.Components.XPosition.update(x_pos + x_velocity) + end + + defp move_x(entity) do + y_pos = <%= app_name %>.Components.YPosition.get(entity) + y_velocity = <%= app_name %>.Components.YVelocity.get(entity) + <%= app_name %>.Components.YPosition.update(x_pos + x_velocity) + end +end diff --git a/test/mix/tasks/ecsx.gen.scaffold_test.exs b/test/mix/tasks/ecsx.gen.scaffold_test.exs new file mode 100644 index 0000000..f7477e5 --- /dev/null +++ b/test/mix/tasks/ecsx.gen.scaffold_test.exs @@ -0,0 +1,249 @@ +Code.require_file("../../support/mix_helper.exs", __DIR__) + +defmodule Mix.Tasks.Ecsx.Gen.ScaffoldTest do + use ExUnit.Case + + import ECSx.MixHelper + + setup do + create_sample_ecsx_project() + on_exit(&clean_tmp_dir/0) + :ok + end + + test "generates system for :movement2d scaffold" do + Mix.Project.in_project(:my_app, ".", fn _module -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["movement2d"]) + + system_file = File.read!("lib/my_app/systems/movement2d.ex") + + assert system_file == + """ + defmodule MyApp.Systems.Movement2d do + @moduledoc \"\"\" + Documentation for Movement2d system. + \"\"\" + @behaviour ECSx.System + + @impl ECSx.System + def run do + # System logic + :ok + end + + # These functions can be used to update the position of entities. + + defp move_x(entity) do + x_pos = MyApp.Components.XPosition.get(entity) + x_velocity = MyApp.Components.XVelocity.get(entity) + MyApp.Components.XPosition.update(x_pos + x_velocity) + end + + defp move_x(entity) do + y_pos = MyApp.Components.YPosition.get(entity) + y_velocity = MyApp.Components.YVelocity.get(entity) + MyApp.Components.YPosition.update(x_pos + x_velocity) + end + end + """ + end) + end + + test "generates system for :health scaffold" do + Mix.Project.in_project(:my_app, ".", fn _module -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["health"]) + + system_file = File.read!("lib/my_app/systems/health.ex") + + assert system_file == + """ + defmodule MyApp.Systems.Health do + @moduledoc \"\"\" + Documentation for Health system. + \"\"\" + @behaviour ECSx.System + + @impl ECSx.System + def run do + # System logic + :ok + end + + # These functions can be used to update the health of entities. + + defp take_damage(entity, damage_points) do + health = MyApp.Components.HealthPoints.get(entity) + MyApp.Components.HealthPoints.update(health - damage_points) + end + + defp add_health(entity, extra_health_points) do + health = MyApp.Components.HealthPoints.get(entity) + MyApp.Components.HealthPoints.update(health + extra_health_points) + end + end + """ + end) + end + + test "injects generated system into manager" do + Mix.Project.in_project(:my_app, ".", fn _module -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["movement2d"]) + + manager_file = File.read!("lib/my_app/manager.ex") + + assert manager_file == + """ + defmodule MyApp.Manager do + @moduledoc \"\"\" + ECSx manager. + \"\"\" + use ECSx.Manager + + def setup do + # Seed persistent components only for the first server start + # (This will not be run on subsequent app restarts) + :ok + end + + def startup do + # Load ephemeral components during first server start and again + # on every subsequent app restart + :ok + end + + # Declare all valid Component types + def components do + [ + MyApp.Components.YVelocity, + MyApp.Components.XVelocity, + MyApp.Components.YPosition, + MyApp.Components.XPosition + ] + end + + # Declare all Systems to run + def systems do + [ + MyApp.Systems.Movement2d + ] + end + end + """ + end) + end + + test "injects generated component(s) into manager" do + Mix.Project.in_project(:my_app, ".", fn _module -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["movement2d"]) + + manager_file = File.read!("lib/my_app/manager.ex") + + assert manager_file == + """ + defmodule MyApp.Manager do + @moduledoc \"\"\" + ECSx manager. + \"\"\" + use ECSx.Manager + + def setup do + # Seed persistent components only for the first server start + # (This will not be run on subsequent app restarts) + :ok + end + + def startup do + # Load ephemeral components during first server start and again + # on every subsequent app restart + :ok + end + + # Declare all valid Component types + def components do + [ + MyApp.Components.YVelocity, + MyApp.Components.XVelocity, + MyApp.Components.YPosition, + MyApp.Components.XPosition + ] + end + + # Declare all Systems to run + def systems do + [ + MyApp.Systems.Movement2d + ] + end + end + """ + end) + end + + test "generates components for :movement2d scaffold" do + Mix.Project.in_project(:my_app, ".", fn _module -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["movement2d"]) + + expected_files = [ + "lib/my_app/components/x_position.ex", + "lib/my_app/components/y_position.ex", + "lib/my_app/components/x_velocity.ex", + "lib/my_app/components/y_velocity.ex" + ] + + assert true = Enum.all?(Enum.map(expected_files, fn path -> File.exists?(path) end)) + + x_position_components_file = File.read!("lib/my_app/components/x_position.ex") + + assert x_position_components_file == + """ + defmodule MyApp.Components.XPosition do + @moduledoc \"\"\" + Documentation for XPosition components. + \"\"\" + use ECSx.Component, + value: :float + end + """ + end) + end + + test "generates components for :health scaffold" do + Mix.Project.in_project(:my_app, ".", fn _module -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["health"]) + + expected_files = [ + "lib/my_app/components/health_points.ex", + "lib/my_app/components/damage_points.ex" + ] + + assert true = Enum.all?(Enum.map(expected_files, fn path -> File.exists?(path) end)) + + health_points_components_file = File.read!("lib/my_app/components/health_points.ex") + + assert health_points_components_file == + """ + defmodule MyApp.Components.HealthPoints do + @moduledoc \"\"\" + Documentation for HealthPoints components. + \"\"\" + use ECSx.Component, + value: :integer + end + """ + end) + end + + test "fails with invalid arguments" do + Mix.Project.in_project(:my_app, ".", fn _module -> + # missing scaffold type + assert_raise(Mix.Error, fn -> + Mix.Tasks.Ecsx.Gen.Scaffold.run([]) + end) + + # invalid scaffold type + assert_raise(Mix.Error, fn -> + Mix.Tasks.Ecsx.Gen.Scaffold.run(["movement3d"]) + end) + end) + end +end