### Run ECSx Setup Generator
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/Installation.md
Execute this command after installing ECSx to run the setup generator, which creates necessary files and folders for your project.
```console
$ mix ecsx.setup
```
--------------------------------
### Create New Phoenix Project
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/Installation.md
Use this command to create a new Elixir/Phoenix project. Assumes Phoenix is installed.
```console
$ mix phx.new my_app
```
--------------------------------
### Ship Manager Module Definition
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Defines the Ship.Manager module, inheriting from ECSx.Manager and outlining key functions for setup, startup, components, and systems.
```elixir
defmodule Ship.Manager do
...
use ECSx.Manager
def setup do
...
end
def startup do
...
end
def components do
...
end
def systems do
...
end
end
```
--------------------------------
### Update Component Read/Write API from get_one to get
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/upgrade_guide.md
In ECSx versions 0.4 and later, `MyComponent.get_one/1` has been renamed to `MyComponent.get/1`. This change applies to how you retrieve single component instances.
```elixir
MyComponent.get(entity)
```
--------------------------------
### Initialize Components from Database in ECSx Manager
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/common_caveats.md
Use `ECSx.Manager.setup/1` to load data from a database at application startup and create components. This avoids slow database queries within game systems.
```elixir
defmodule MyApp.Manager do
use ECSx.Manager
alias MyApp.Components.Height
alias MyApp.Components.XPosition
alias MyApp.Components.YPosition
alias MyApp.Trees
alias MyApp.Trees.Tree
setup do
for %Tree{id: id, x: x, y: y, height: height} <- Trees.my_db_query() do
XPosition.add(id, x)
YPosition.add(id, y)
Height.add(id, height)
end
end
...
end
```
--------------------------------
### Initialize Game State and Player in LiveView
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Configures the initial state of the game, including player entity, screen dimensions, and loading status. It also initiates the first load sequence for game elements.
```elixir
defmodule ShipWeb.GameLive do
...
alias Ship.Components.ImageFile
...
def mount(_params, %{"player_token" => token} = _session, socket) do
player = Ship.Players.get_player_by_session_token(token)
socket =
socket
|> assign(player_entity: player.id)
|> assign(keys: MapSet.new())
# These will configure the scale of our display compared to the game world
|> assign(game_world_size: 100, screen_height: 30, screen_width: 50)
|> assign_loading_state()
if connected?(socket) do
ECSx.ClientEvents.add(player.id, :spawn_ship)
send(self(), :first_load)
end
{:ok, socket}
end
defp assign_loading_state(socket) do
assign(socket,
x_coord: nil,
y_coord: nil,
current_hp: nil,
player_ship_image_file: nil,
other_ships: [],
x_offset: 0,
y_offset: 0,
loading: true
)
end
def handle_info(:first_load, socket) do
:ok = wait_for_spawn(socket.assigns.player_entity)
socket =
socket
|> assign_player_ship()
|> assign_other_ships()
|> assign_offsets()
|> assign(loading: false)
:timer.send_interval(50, :refresh)
{:noreply, socket}
end
def handle_info(:refresh, socket) do
socket =
socket
|> assign_player_ship()
|> assign_other_ships()
|> assign_offsets()
{:noreply, socket}
end
defp wait_for_spawn(player_entity) do
if PlayerSpawned.exists?(player_entity) do
:ok
else
Process.sleep(10)
wait_for_spawn(player_entity)
end
end
defp assign_player_ship(socket) do
x = XPosition.get(socket.assigns.player_entity)
y = YPosition.get(socket.assigns.player_entity)
hp = HullPoints.get(socket.assigns.player_entity)
image = ImageFile.get(socket.assigns.player_entity)
assign(socket, x_coord: x, y_coord: y, current_hp: hp, player_ship_image_file: image)
end
defp assign_other_ships(socket) do
other_ships =
Enum.reject(all_ships(), fn {entity, _, _, _} -> entity == socket.assigns.player_entity end)
assign(socket, other_ships: other_ships)
end
defp all_ships do
for {ship, _hp} <- HullPoints.get_all() do
x = XPosition.get(ship)
y = YPosition.get(ship)
image = ImageFile.get(ship)
{ship, x, y, image}
end
end
defp assign_offsets(socket) do
# Note: the socket must already have updated player coordinates before assigning offsets!
%{screen_width: screen_width, screen_height: screen_height} = socket.assigns
%{x_coord: x, y_coord: y, game_world_size: game_world_size} = socket.assigns
x_offset = calculate_offset(x, screen_width, game_world_size)
y_offset = calculate_offset(y, screen_height, game_world_size)
assign(socket, x_offset: x_offset, y_offset: y_offset)
end
defp calculate_offset(coord, screen_size, game_world_size) do
case coord - div(screen_size, 2) do
offset when offset < 0 -> 0
offset when offset > game_world_size - screen_size -> game_world_size - screen_size
offset -> offset
end
end
def handle_event("keydown", %{"key" => key}, socket) do
...
end
```
--------------------------------
### Fetch Project Dependencies
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/Installation.md
After adding ECSx to your dependencies, run this command from your project's root directory to fetch all dependencies.
```console
$ mix deps.get
```
--------------------------------
### Create New Elixir Project with Supervision
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/Installation.md
Use this command to create a new Elixir project with a supervision tree. Useful for standalone Elixir applications.
```console
$ mix new my_app --sup
```
--------------------------------
### Add ImageFile Component on Ship Startup
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Adds the 'npc_ship.svg' ImageFile component to entities during the ship startup process.
```elixir
defmodule Ship.Manager do
...
def startup do
for _ships <- 1..40 do
...
Ship.Components.ImageFile.add(entity, "npc_ship.svg")
end
end
...
end
```
--------------------------------
### Update GameLive Mount and HandleInfo
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Replaces the existing mount and handle_info functions in ShipWeb.GameLive. It initializes player data, assigns loading state, and triggers the first load sequence after the player has spawned.
```elixir
defmodule ShipWeb.GameLive do
...
alias Ship.Components.PlayerSpawned
...
def mount(_params, %{"player_token" => token} = _session, socket) do
player = Ship.Players.get_player_by_session_token(token)
socket =
socket
|> assign(player_entity: player.id)
|> assign(keys: MapSet.new())
# This gets its own helper in case we need to return to this state again later
|> assign_loading_state()
if connected?(socket) do
ECSx.ClientEvents.add(player.id, :spawn_ship)
# The first load will now have additional responsibilities
send(self(), :first_load)
end
{:ok, socket}
end
defp assign_loading_state(socket) do
assign(socket,
x_coord: nil,
y_coord: nil,
current_hp: nil,
# This new assign will control whether the loading screen is shown
loading: true
)
end
def handle_info(:first_load, socket) do
# Don't start fetching components until after spawn is complete!
:ok = wait_for_spawn(socket.assigns.player_entity)
socket =
socket
|> assign_player_ship()
|> assign(loading: false)
# We want to keep up-to-date on this info
:timer.send_interval(50, :refresh)
{:noreply, socket}
end
def handle_info(:refresh, socket) do
{:noreply, assign_player_ship(socket)}
end
defp wait_for_spawn(player_entity) do
if PlayerSpawned.exists?(player_entity) do
:ok
else
Process.sleep(10)
wait_for_spawn(player_entity)
end
end
# Our previous :load_player_info handler becomes a shared helper for the new handlers
defp assign_player_ship(socket) do
x = XPosition.get(socket.assigns.player_entity)
y = YPosition.get(socket.assigns.player_entity)
hp = HullPoints.get(socket.assigns.player_entity)
assign(socket, x_coord: x, y_coord: y, current_hp: hp)
end
def handle_event("keydown", %{"key" => key}, socket) do
...
```
--------------------------------
### Adding Components to an Entity
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/ecs_design.md
Demonstrates how to add initial components to an entity. This is a basic way to assign attributes like weapons or shields.
```elixir
Weapon.add(hero_entity, "Longsword")
Shield.add(hero_entity, "Buckler")
```
--------------------------------
### Generate System
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Use this command to generate a new system module. This creates the basic structure for organizing game logic.
```bash
$ mix ecsx.gen.system Driver
```
--------------------------------
### Initializing Ship Components
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generates forty unique entities and adds various ship components to each, including armor, damage, range, speed, position, and velocity.
```elixir
def startup do
for _ships <- 1..40 do
# First generate a unique ID to represent the new entity
entity = Ecto.UUID.generate()
# Then use that ID to create the components which make up a ship
Ship.Components.ArmorRating.add(entity, 0)
Ship.Components.AttackDamage.add(entity, 5)
Ship.Components.AttackRange.add(entity, 10)
Ship.Components.AttackSpeed.add(entity, 1.05)
Ship.Components.HullPoints.add(entity, 50)
Ship.Components.SeekingTarget.add(entity)
Ship.Components.XPosition.add(entity, Enum.random(1..100))
Ship.Components.YPosition.add(entity, Enum.random(1..100))
Ship.Components.XVelocity.add(entity, 0)
Ship.Components.YVelocity.add(entity, 0)
end
end
```
--------------------------------
### Render Game Entities with SVG in LiveView
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
This snippet shows how to render the game world using SVG within a LiveView template. It includes setting up the viewBox for camera control, rendering a background rectangle, displaying a loading message, and conditionally rendering player and other ships as images. It also shows how to display player Hull Points.
```elixir
defmodule ShipWeb.GameLive do
...
def render(assigns) do
~H"""
"""
end
end
```
--------------------------------
### Generate ECS Components, Systems, and Tags
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Commands to generate the necessary components, systems, and tags for projectile functionality.
```bash
$
mix ecsx.gen.component ProjectileTarget binary
mix ecsx.gen.component ProjectileDamage integer
mix ecsx.gen.system Projectile
mix ecsx.gen.tag IsProjectile
```
--------------------------------
### Generate Player Authentication
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Use the Phoenix Auth generator to set up player authentication and manage player data. This command expects players to register with an email and password and creates a unique ID for each player.
```bash
$ mix phx.gen.auth Players Player players --binary-id
$ mix deps.get
$ mix ecto.migrate
```
--------------------------------
### Game LiveView Module
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
This module handles the game's live view, managing player authentication, spawning ships, and processing player input for movement. It uses ECSx.ClientEvents to communicate frontend actions to the backend.
```elixir
defmodule ShipWeb.GameLive do
use ShipWeb, :live_view
alias Ship.Components.HullPoints
alias Ship.Components.XPosition
alias Ship.Components.YPosition
def mount(_params, %{"player_token" => token} = _session, socket) do
# This context function was generated by phx.gen.auth
player = Ship.Players.get_player_by_session_token(token)
socket =
socket
|> assign(player_entity: player.id)
# Keeping a set of currently held keys will allow us to prevent duplicate keydown events
|> assign(keys: MapSet.new())
# We don't know where the ship will spawn, yet
|> assign(x_coord: nil, y_coord: nil, current_hp: nil)
# We don't want these calls to be made on both the initial static page render and again after
# the LiveView is connected, so we wrap them in `connected?/1` to prevent duplication
if connected?(socket) do
ECSx.ClientEvents.add(player.id, :spawn_ship)
:timer.send_interval(50, :load_player_info)
end
{:ok, socket}
end
def handle_info(:load_player_info, socket) do
# This will run every 50ms to keep the client assigns updated
x = XPosition.get(socket.assigns.player_entity)
y = YPosition.get(socket.assigns.player_entity)
hp = HullPoints.get(socket.assigns.player_entity)
{:noreply, assign(socket, x_coord: x, y_coord: y, current_hp: hp)}
end
def handle_event("keydown", %{"key" => key}, socket) do
if MapSet.member?(socket.assigns.keys, key) do
# Already holding this key - do nothing
{:noreply, socket}
else
# We only want to add a client event if the key is defined by the `keydown/1` helper below
maybe_add_client_event(socket.assigns.player_entity, key, &keydown/1)
{:noreply, assign(socket, keys: MapSet.put(socket.assigns.keys, key))}
end
end
def handle_event("keyup", %{"key" => key}, socket) do
# We don't have to worry about duplicate keyup events
# But once again, we will only add client events for keys that actually do something
maybe_add_client_event(socket.assigns.player_entity, key, &keyup/1)
{:noreply, assign(socket, keys: MapSet.delete(socket.assigns.keys, key))}
end
defp maybe_add_client_event(player_entity, key, fun) do
case fun.(key) do
:noop -> :ok
event -> ECSx.ClientEvents.add(player_entity, event)
end
end
defp keydown(key) when key in ~w(w W ArrowUp), do: {:move, :north}
defp keydown(key) when key in ~w(a A ArrowLeft), do: {:move, :west}
defp keydown(key) when key in ~w(s S ArrowDown), do: {:move, :south}
defp keydown(key) when key in ~w(d D ArrowRight), do: {:move, :east}
defp keydown(_key), do: :noop
defp keyup(key) when key in ~w(w W ArrowUp), do: {:stop_move, :north}
defp keyup(key) when key in ~w(a A ArrowLeft), do: {:stop_move, :west}
defp keyup(key) when key in ~w(s S ArrowDown), do: {:stop_move, :south}
defp keyup(key) when key in ~w(d D ArrowRight), do: {:stop_move, :east}
defp keyup(_key), do: :noop
def render(assigns) do
~H"""
Player ID: <%= @player_entity %>
Player Coords: <%= inspect({@x_coord, @y_coord}) %>
Hull Points: <%= @current_hp %>
"""
end
end
```
--------------------------------
### Client Event Handler System
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
This system processes events sent from the LiveView client. It handles spawning ships with specific stats and processing player movement commands.
```elixir
defmodule Ship.Systems.ClientEventHandler do
...
@behaviour ECSx.System
alias Ship.Components.ArmorRating
alias Ship.Components.AttackDamage
alias Ship.Components.AttackRange
alias Ship.Components.AttackSpeed
alias Ship.Components.HullPoints
alias Ship.Components.SeekingTarget
alias Ship.Components.XPosition
alias Ship.Components.XVelocity
alias Ship.Components.YPosition
alias Ship.Components.YVelocity
@impl ECSx.System
def run do
client_events = ECSx.ClientEvents.get_and_clear()
Enum.each(client_events, &process_one/1)
end
defp process_one({player, :spawn_ship}) do
# We'll give player ships better stats than the enemy ships
# (otherwise the game would be very short!)
ArmorRating.add(player, 2)
AttackDamage.add(player, 6)
AttackRange.add(player, 15)
AttackSpeed.add(player, 1.2)
HullPoints.add(player, 75)
SeekingTarget.add(player)
XPosition.add(player, Enum.random(1..100))
YPosition.add(player, Enum.random(1..100))
XVelocity.add(player, 0)
YVelocity.add(player, 0)
end
# Note Y movement will use screen position (increasing Y goes south)
defp process_one({player, {:move, :north}}), do: YVelocity.update(player, -1)
defp process_one({player, {:move, :south}}), do: YVelocity.update(player, 1)
defp process_one({player, {:move, :east}}), do: XVelocity.update(player, 1)
defp process_one({player, {:move, :west}}), do: XVelocity.update(player, -1)
defp process_one({player, {:stop_move, :north}}), do: YVelocity.update(player, 0)
defp process_one({player, {:stop_move, :south}}), do: YVelocity.update(player, 0)
defp process_one({player, {:stop_move, :east}}), do: XVelocity.update(player, 0)
defp process_one({player, {:stop_move, :west}}), do: XVelocity.update(player, 0)
end
```
--------------------------------
### Add Player Ship ImageFile Component on Spawn
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Adds the 'player_ship.svg' ImageFile component to a player entity when it spawns.
```elixir
defmodule Ship.Systems.ClientEventHandler do
...
alias Ship.Components.ImageFile
...
defp process_one({player, :spawn_ship}) do
...
ImageFile.add(player, "player_ship.svg")
PlayerSpawned.add(player)
end
...
end
```
--------------------------------
### Generate Integer Component Type
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Use this command to generate a new integer component type for your game entities. This sets up the necessary module and interface for handling this component.
```bash
$ mix ecsx.gen.component HullPoints integer
```
```bash
$ mix ecsx.gen.component ArmorRating integer
```
```bash
$ mix ecsx.gen.component AttackDamage integer
```
```bash
$ mix ecsx.gen.component AttackRange integer
```
```bash
$ mix ecsx.gen.component XPosition integer
```
```bash
$ mix ecsx.gen.component YPosition integer
```
```bash
$ mix ecsx.gen.component XVelocity integer
```
```bash
$ mix ecsx.gen.component YVelocity integer
```
--------------------------------
### Targeting System Implementation
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
The Targeting system iterates through entities with SeekingTarget, finds the closest valid enemy within attack range, and replaces SeekingTarget with AttackTarget.
```elixir
defmodule Ship.Systems.Targeting do
...
@behaviour ECSx.System
alias Ship.Components.AttackRange
alias Ship.Components.AttackTarget
alias Ship.Components.HullPoints
alias Ship.Components.SeekingTarget
alias Ship.SystemUtils
@impl ECSx.System
def run do
entities = SeekingTarget.get_all()
Enum.each(entities, &attempt_target/1)
end
defp attempt_target(self) do
case look_for_target(self) do
nil -> :noop
{target, _hp} -> add_target(self, target)
end
end
defp look_for_target(self) do
# For now, we're assuming anything which has HullPoints can be attacked
HullPoints.get_all()
# ... except your own ship!
|> Enum.reject(fn {possible_target, _hp} -> possible_target == self end)
|> Enum.find(fn {possible_target, _hp} ->
distance_between = SystemUtils.distance_between(possible_target, self)
range = AttackRange.get(self)
distance_between < range
end)
end
defp add_target(self, target) do
SeekingTarget.remove(self)
AttackTarget.add(self, target)
end
end
```
--------------------------------
### Configure Persistence Adapter
Source: https://github.com/ecsx-framework/ecsx/blob/master/README.md
Configure the persistence adapter for ECSx by setting the 'persistence_adapter' application variable in your config.exs.
```elixir
config :ecsx,
...
persistence_adapter: ...
```
--------------------------------
### Generate AttackTarget and AttackCooldown Components
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generate components to store the target entity's ID and the cooldown expiration datetime. Adjust types (binary, datetime) as needed for your project.
```bash
$ mix ecsx.gen.component AttackTarget binary
$ mix ecsx.gen.component AttackCooldown datetime
```
--------------------------------
### Render GameLive with Loading State
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Updates the render function in ShipWeb.GameLive to conditionally display a 'Loading...' message or the player's game information based on the `@loading` assign. This controls the visibility of the loading screen.
```elixir
def render(assigns) do
~H"""
<%= if @loading do %>
Loading...
<% else %>
Player ID: <%= @player_entity %>
Player Coords: <%= inspect({@x_coord, @y_coord}) %>
Hull Points: <%= @current_hp %>
<% end %>
"""
end
```
--------------------------------
### Elixir System for Ship Targeting and Attacking
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
This Elixir module implements an ECSx system for handling ship attacks. It iterates through entities with attack targets, checks range and cooldowns, and deals damage. Use this as a foundation for your game's combat mechanics.
```elixir
defmodule Ship.Systems.Attacking do
...
@behaviour ECSx.System
alias Ship.Components.ArmorRating
alias Ship.Components.AttackCooldown
alias Ship.Components.AttackDamage
alias Ship.Components.AttackRange
alias Ship.Components.AttackSpeed
alias Ship.Components.AttackTarget
alias Ship.Components.HullPoints
alias Ship.Components.SeekingTarget
alias Ship.SystemUtils
@impl ECSx.System
def run do
attack_targets = AttackTarget.get_all()
Enum.each(attack_targets, &attack_if_ready/1)
end
defp attack_if_ready({self, target}) do
cond do
SystemUtils.distance_between(self, target) > AttackRange.get(self) ->
# If the target ever leaves our attack range, we want to remove the AttackTarget
# and begin searching for a new one.
AttackTarget.remove(self)
SeekingTarget.add(self)
AttackCooldown.exists?(self) ->
# We're still within range, but waiting on the cooldown
:noop
:otherwise ->
deal_damage(self, target)
add_cooldown(self)
end
end
defp deal_damage(self, target) do
attack_damage = AttackDamage.get(self)
# Assuming one armor rating always equals one damage
reduction_from_armor = ArmorRating.get(target)
final_damage_amount = attack_damage - reduction_from_armor
target_current_hp = HullPoints.get(target)
target_new_hp = target_current_hp - final_damage_amount
HullPoints.update(target, target_new_hp)
end
defp add_cooldown(self) do
now = DateTime.utc_now()
ms_between_attacks = calculate_cooldown_time(self)
cooldown_until = DateTime.add(now, ms_between_attacks, :millisecond)
AttackCooldown.add(self, cooldown_until)
end
# We're going to model AttackSpeed with a float representing attacks per second.
# The goal here is to convert that into milliseconds per attack.
defp calculate_cooldown_time(self) do
attacks_per_second = AttackSpeed.get(self)
seconds_per_attack = 1 / attacks_per_second
ceil(seconds_per_attack * 1000)
end
end
```
--------------------------------
### Generate SeekingTarget Tag Component
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Use this command to generate a Tag component for entities that are seeking a target. No additional data is needed for this component.
```bash
$ mix ecsx.gen.tag SeekingTarget
```
--------------------------------
### Generate Targeting System
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generates the Targeting system, which finds and assigns targets to entities possessing the SeekingTarget component.
```bash
$ mix ecsx.gen.system Targeting
```
--------------------------------
### Render Projectiles and Ships in LiveView
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
This snippet shows how to iterate over lists of projectiles and other ships to render them as image elements within a LiveView template. It uses Elixir's `~H` sigil for HTML templating.
```elixir
defmodule Ship.GameLive do
def render(assigns) do
~H"""
...
<%= for {_entity, x, y, image_file} <- @projectiles do %>
<% end %>
<%= for {_entity, x, y, image_file} <- @other_ships do %>
...
"""
end
end
```
--------------------------------
### Generate Float Component Type
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Use this command to generate a new float component type for your game entities. This sets up the necessary module and interface for handling this component.
```bash
$ mix ecsx.gen.component AttackSpeed float
```
--------------------------------
### Generate Attacking System
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generates the Attacking system, responsible for executing attacks on assigned targets if they are still in range and not on cooldown.
```bash
$ mix ecsx.gen.system Attacking
```
--------------------------------
### Define Game Route in Phoenix
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Add a new live route for the game interface within the router configuration. This route requires players to be authenticated before accessing the game.
```elixir
scope "/", ShipWeb do
pipe_through [:browser, :require_authenticated_player]
live_session :require_authenticated_player,
on_mount: [{ShipWeb.PlayerAuth, :ensure_authenticated}] do
live "/game", GameLive
...
end
end
```
--------------------------------
### Representing Complex Enchantments as Entities
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/ecs_design.md
Demonstrates how to model complex or multiple enchantments as separate entities, linking them to the target item. This provides a flexible way to handle arbitrary enchantment details.
```elixir
enchantment_entity = Ecto.UUID.generate()
Description.add(enchantment_entity, "Firaga")
EnchantTarget.add(enchantment_entity, sword_entity)
```
--------------------------------
### Add ECSx Dependency
Source: https://github.com/ecsx-framework/ecsx/blob/master/README.md
Add the ECSx dependency to your project's mix.exs file to include the framework.
```elixir
def deps do
[
{:ecsx, "~> 0.5"}
]
end
```
--------------------------------
### Generate Destruction System
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generates a system to handle entity destruction. This system removes components and updates targeting when an entity's HP drops to zero or below.
```elixir
defmodule Ship.Systems.Destruction do
...
@behaviour ECSx.System
alias Ship.Components.ArmorRating
alias Ship.Components.AttackCooldown
alias Ship.Components.AttackDamage
alias Ship.Components.AttackRange
alias Ship.Components.AttackSpeed
alias Ship.Components.AttackTarget
alias Ship.Components.DestroyedAt
alias Ship.Components.HullPoints
alias Ship.Components.SeekingTarget
alias Ship.Components.XPosition
alias Ship.Components.XVelocity
alias Ship.Components.YPosition
alias Ship.Components.YVelocity
@impl ECSx.System
def run do
ships = HullPoints.get_all()
Enum.each(ships, fn {entity, hp} ->
if hp <= 0, do: destroy(entity)
end)
end
defp destroy(ship) do
ArmorRating.remove(ship)
AttackCooldown.remove(ship)
AttackDamage.remove(ship)
AttackRange.remove(ship)
AttackSpeed.remove(ship)
AttackTarget.remove(ship)
HullPoints.remove(ship)
SeekingTarget.remove(ship)
XPosition.remove(ship)
XVelocity.remove(ship)
YPosition.remove(ship)
YVelocity.remove(ship)
# when a ship is destroyed, other ships should stop targeting it
untarget(ship)
DestroyedAt.add(ship, DateTime.utc_now())
end
defp untarget(target) do
for ship <- AttackTarget.search(target) do
AttackTarget.remove(ship)
SeekingTarget.add(ship)
end
end
end
```
--------------------------------
### Add PlayerSpawned Tag in ClientEventHandler
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Integrates the PlayerSpawned tag by adding it to the player entity at the end of the :spawn_ship client event processing. This signals that the ship has successfully spawned.
```elixir
defmodule Ship.Systems.ClientEventHandler do
...
alias Ship.Components.PlayerSpawned
...
defp process_one({player, :spawn_ship}) do
...
PlayerSpawned.add(player)
end
...
end
```
--------------------------------
### Creating Separate Entities for Associated Items
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/ecs_design.md
Illustrates creating new entities to represent associated items like swords, linking them back to the owner entity. This approach supports multiple items and complex attributes.
```elixir
sword_entity = Ecto.UUID.generate()
Description.add(sword_entity, "Longsword")
EquippedBy.add(sword_entity, hero_entity)
```
```elixir
another_sword_entity = Ecto.UUID.generate()
Description.add(another_sword_entity, "Shortsword")
EquippedBy.add(another_sword_entity, hero_entity)
```
--------------------------------
### Update Attacking System to Spawn Projectiles
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Modifies the Attacking system to spawn a projectile entity instead of dealing damage directly. This involves replacing the `deal_damage/2` function with `spawn_projectile/2`.
```elixir
defmodule Ship.Systems.Attacking do
...
@behaviour ECSx.System
alias Ship.Components.AttackCooldown
alias Ship.Components.AttackDamage
alias Ship.Components.AttackRange
alias Ship.Components.AttackSpeed
alias Ship.Components.AttackTarget
alias Ship.Components.ImageFile
alias Ship.Components.IsProjectile
alias Ship.Components.ProjectileDamage
alias Ship.Components.ProjectileTarget
alias Ship.Components.SeekingTarget
alias Ship.Components.XPosition
alias Ship.Components.XVelocity
alias Ship.Components.YPosition
alias Ship.Components.YVelocity
alias Ship.SystemUtils
...
defp attack_if_ready({self, target}) do
cond do
...
:otherwise ->
spawn_projectile(self, target)
add_cooldown(self)
end
end
defp spawn_projectile(self, target) do
attack_damage = AttackDamage.get(self)
x = XPosition.get(self)
y = YPosition.get(self)
# Armor reduction should wait until impact to be calculated
cannonball_entity = Ecto.UUID.generate()
IsProjectile.add(cannonball_entity)
XPosition.add(cannonball_entity, x)
YPosition.add(cannonball_entity, y)
XVelocity.add(cannonball_entity, 0)
YVelocity.add(cannonball_entity, 0)
ImageFile.add(cannonball_entity, "cannonball.svg")
ProjectileTarget.add(cannonball_entity, target)
ProjectileDamage.add(cannonball_entity, attack_damage)
end
...
end
```
--------------------------------
### Generate Cooldown Expiration System
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generates a new system for managing cooldown expirations. This system is useful for time-based component removal.
```elixir
defmodule Ship.Systems.CooldownExpiration do
...
@behaviour ECSx.System
alias Ship.Components.AttackCooldown
@impl ECSx.System
def run do
now = DateTime.utc_now()
cooldowns = AttackCooldown.get_all()
Enum.each(cooldowns, &remove_when_expired(&1, now))
end
defp remove_when_expired({entity, timestamp}, now) do
case DateTime.compare(now, timestamp) do
:lt -> :noop
_ -> AttackCooldown.remove(entity)
end
end
end
```
--------------------------------
### Create a Basic SVG Circle
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
This HTML code defines a basic SVG circle. The `width` and `height` attributes will be overridden by LiveView, but they influence the relative sizing of SVG elements like the circle's center and radius.
```html
```
--------------------------------
### Implement Driver System for Position Update
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
This Elixir code implements the 'Driver' system, which updates the X and Y positions of entities based on their respective velocities. It iterates through all entities with velocity components and applies the positional changes.
```elixir
defmodule Ship.Systems.Driver do
...
@behaviour ECSx.System
alias Ship.Components.XPosition
alias Ship.Components.YPosition
alias Ship.Components.XVelocity
alias Ship.Components.YVelocity
@impl ECSx.System
def run do
for {entity, x_velocity} <- XVelocity.get_all() do
x_position = XPosition.get(entity)
new_x_position = x_position + x_velocity
XPosition.update(entity, new_x_position)
end
# Once the x-values are updated, do the same for the y-values
for {entity, y_velocity} <- YVelocity.get_all() do
y_position = YPosition.get(entity)
new_y_position = y_position + y_velocity
YPosition.update(entity, new_y_position)
end
end
end
```
--------------------------------
### Generate Destruction Component
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
Generates a new component to mark entities with a destruction timestamp. This is useful for tracking when an entity was destroyed.
```elixir
mix ecsx.gen.component DestroyedAt datetime
```
--------------------------------
### Add ECSx Dependency to Mix Project
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/Installation.md
Add the ECSx framework to your project's dependencies by including it in the `deps` function in `mix.exs`.
```elixir
defp deps do
[
{:ecsx, "~> 0.5"}
]
end
```
--------------------------------
### Update Component Update API from add to update
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/upgrade_guide.md
For unique components in ECSx versions 0.3.x to 0.4, use `MyComponent.update(entity, value)` instead of `MyComponent.add(entity, value)` for updating existing component values. If `add/2` was used for both creation and updates, separate these into distinct `add/2` for creation and `update/2` for updates.
```elixir
MyComponent.update(entity, value)
```
--------------------------------
### Update Destruction System for Projectile Targets
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Extends the `Destruction` system to include an `untarget` feature for `ProjectileTarget` components. This ensures projectiles are properly de-referenced when their target is destroyed.
```elixir
defmodule Ship.Systems.Destruction do
...
alias Ship.Components.ProjectileTarget
...
defp untarget(target) do
for ship <- AttackTarget.search(target) do
AttackTarget.remove(ship)
SeekingTarget.add(ship)
end
for projectile <- ProjectileTarget.search(target) do
ProjectileTarget.remove(projectile)
end
end
end
```
--------------------------------
### Update Component Read/Write API for Optional Components
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/upgrade_guide.md
In ECSx versions 0.3.x to 0.4, calls to `MyComponent.get_one(entity)` that might return `nil` should now explicitly include `nil` as the second argument: `MyComponent.get_one(entity, nil)`.
```elixir
MyComponent.get_one(entity, nil)
```
--------------------------------
### Adding Attributes to Associated Entities
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/ecs_design.md
Shows how to add specific attributes, such as durability, to entities that represent associated items. This allows for detailed item management.
```elixir
Durability.add(sword_entity, 150)
Durability.add(another_sword_entity, 75)
```
--------------------------------
### Assign Projectiles to LiveView State
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Adds a `projectiles` assign to the LiveView socket, populating it with data from all active projectiles. This is used for rendering projectiles on the frontend.
```elixir
defmodule Ship.GameLive do
...
alias Ship.Components.IsProjectile
...
defp assign_loading_state(socket) do
assign(socket,
...
projectiles: []
)
end
def handle_info(:first_load, socket) do
...
socket =
socket
|> assign_player_ship()
|> assign_other_ships()
|> assign_projectiles()
|> assign_offsets()
|> assign(loading: false)
...
end
def handle_info(:refresh, socket) do
socket =
socket
|> assign_player_ship()
|> assign_other_ships()
|> assign_projectiles()
|> assign_offsets()
...
end
...
defp assign_projectiles(socket) do
projectiles =
for projectile <- IsProjectile.get_all() do
x = XPosition.get(projectile)
y = YPosition.get(projectile)
image = ImageFile.get(projectile)
{projectile, x, y, image}
end
assign(socket, projectiles: projectiles)
end
...
end
```
--------------------------------
### Projectile System Logic
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Defines the ECSx system for managing projectile behavior, including seeking targets, moving, and handling collisions. It determines projectile actions based on distance to the target and applies damage upon impact.
```elixir
defmodule Ship.Systems.Projectile do
...
@behaviour ECSx.System
alias Ship.Components.ArmorRating
alias Ship.Components.HullPoints
alias Ship.Components.ImageFile
alias Ship.Components.IsProjectile
alias Ship.Components.ProjectileDamage
alias Ship.Components.ProjectileTarget
alias Ship.Components.XPosition
alias Ship.Components.XVelocity
alias Ship.Components.YPosition
alias Ship.Components.YVelocity
@cannonball_speed 3
@impl ECSx.System
def run do
projectiles = IsProjectile.get_all()
Enum.each(projectiles, fn projectile ->
case ProjectileTarget.get(projectile, nil) do
nil ->
# The target has already been destroyed
destroy_projectile(projectile)
target ->
continue_seeking_target(projectile, target)
end
end)
end
defp continue_seeking_target(projectile, target) do
{dx, dy, distance} = get_distance_to_target(projectile, target)
case distance do
0 ->
collision(projectile, target)
distance when distance / @cannonball_speed <= 1 ->
move_directly_to_target(projectile, {dx, dy})
distance ->
adjust_velocity_towards_target(projectile, {distance, dx, dy})
end
end
defp get_distance_to_target(projectile, target) do
target_x = XPosition.get(target)
target_y = YPosition.get(target)
target_dx = XVelocity.get(target)
target_dy = YVelocity.get(target)
target_next_x = target_x + target_dx
target_next_y = target_y + target_dy
x = XPosition.get(projectile)
y = YPosition.get(projectile)
dx = target_next_x - x
dy = target_next_y - y
{dx, dy, ceil(:math.sqrt(dx ** 2 + dy ** 2))}
end
defp collision(projectile, target) do
damage_target(projectile, target)
destroy_projectile(projectile)
end
defp damage_target(projectile, target) do
damage = ProjectileDamage.get(projectile)
reduction_from_armor = ArmorRating.get(target)
final_damage_amount = damage - reduction_from_armor
target_current_hp = HullPoints.get(target)
target_new_hp = target_current_hp - final_damage_amount
HullPoints.update(target, target_new_hp)
end
defp destroy_projectile(projectile) do
IsProjectile.remove(projectile)
XPosition.remove(projectile)
YPosition.remove(projectile)
XVelocity.remove(projectile)
YVelocity.remove(projectile)
ImageFile.remove(projectile)
ProjectileTarget.remove(projectile)
ProjectileDamage.remove(projectile)
end
defp move_directly_to_target(projectile, {dx, dy}) do
XVelocity.update(projectile, dx)
YVelocity.update(projectile, dy)
end
defp adjust_velocity_towards_target(projectile, {distance, dx, dy}) do
# We know what is needed, but we need to slow it down, so its travel
# will take more than one tick. Otherwise the player will not see it!
ticks_away = ceil(distance / @cannonball_speed)
adjusted_dx = div(dx, ticks_away)
adjusted_dy = div(dy, ticks_away)
XVelocity.update(projectile, adjusted_dx)
YVelocity.update(projectile, adjusted_dy)
end
end
```
--------------------------------
### Add PlayerSpawned Tag
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
Generates a new ECSx tag named PlayerSpawned using the mix ecsx.gen.tag command. This tag will be used to mark when a player's ship has finished spawning.
```bash
$ mix ecsx.gen.tag PlayerSpawned
```
--------------------------------
### Limit Ship Movement within Map Boundaries
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/web_frontend_liveview.md
This Elixir code limits the player ship's position to be between 0 and 99 on both the X and Y axes. It ensures the ship stays within the 100x100 world map boundaries.
```elixir
defmodule Ship.Systems.Driver do
...
def run do
for {entity, x_velocity} <- XVelocity.get_all() do
...
new_x_position = calculate_new_position(x_position, x_velocity)
...
end
for {entity, y_velocity} <- YVelocity.get_all() do
...
new_y_position = calculate_new_position(y_position, y_velocity)
...
end
...
end
# Do not let player ship move past the map limit
defp calculate_new_position(current_position, velocity) do
new_position = current_position + velocity
new_position = Enum.min([new_position, 99])
Enum.max([new_position, 0])
end
end
```
--------------------------------
### Calculate Distance Between Entities
Source: https://github.com/ecsx-framework/ecsx/blob/master/guides/tutorial/backend_basics.md
A utility function to calculate the Euclidean distance between two entities using their X and Y position components. This is a shared mathematical helper for systems.
```elixir
defmodule Ship.SystemUtils do
@moduledoc """
Useful math functions used by multiple systems.
"""
alias Ship.Components.XPosition
alias Ship.Components.YPosition
def distance_between(entity_1, entity_2) do
x_1 = XPosition.get(entity_1)
x_2 = XPosition.get(entity_2)
y_1 = YPosition.get(entity_1)
y_2 = YPosition.get(entity_2)
x = abs(x_1 - x_2)
y = abs(y_1 - y_2)
:math.sqrt(x ** 2 + y ** 2)
end
end
```
=== COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.