-
Notifications
You must be signed in to change notification settings - Fork 5
Seeking AI behaviour #18
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
base: master
Are you sure you want to change the base?
Changes from all commits
3d72012
e9913c3
59fa130
3792cbd
1fd1a23
239f2cc
165397c
f382506
e9c9693
6fdee5b
f15a24f
b2a2252
12af7b6
260666f
f718ddf
38b65ca
f819c1d
f7ab019
0c18581
d93eeb8
e6dc0a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| defmodule Entice.Logic.Seek do | ||
| alias Entice.Entity | ||
| alias Entice.Logic.{Seek, Player.Position, Npc, Movement} | ||
| alias Geom.Shape.{Path, Vector} | ||
| alias Geom.Ai.Astar | ||
|
|
||
| defstruct target: nil, aggro_distance: 1000, escape_distance: 2000, path: [] | ||
|
|
||
| def register(entity), | ||
| do: Entity.put_behaviour(entity, Seek.Behaviour, []) | ||
|
|
||
| def register(entity, aggro_distance, escape_distance) | ||
| when is_integer(aggro_distance) and is_integer(escape_distance), | ||
| do: Entity.put_behaviour(entity, Seek.Behaviour, %{aggro_distance: aggro_distance, escape_distance: escape_distance}) | ||
|
|
||
| def unregister(entity), | ||
| do: Entity.remove_behaviour(entity, Seek.Behaviour) | ||
|
|
||
| #TODO: Add team attr to determine who should be attacked by whom | ||
| defmodule Behaviour do | ||
| use Entice.Entity.Behaviour | ||
|
|
||
| def init(entity, %{aggro_distance: aggro_distance, escape_distance: escape_distance}), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might want to check already here if all the attributes we require being present are actually there...
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what you mean by that. We match on aggro_distance and escape_distance, if they're not there we get the default init, no?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No I meant Attributes, as in if the entity has the attributes we need, i.e. Movement etc. |
||
| do: {:ok, entity |> put_attribute(%Seek{aggro_distance: aggro_distance, escape_distance: escape_distance})} | ||
|
|
||
| def init(entity, _args), | ||
| do: {:ok, entity |> put_attribute(%Seek{})} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above, no args = what behaviour?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right I'll add an init that only take an entity
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I meant: Do we need this case? Is this actually any use without parameters? |
||
|
|
||
| #No introspection for npcs ;) | ||
| def handle_event({:entity_change, %{entity_id: eid}}, %Entity{id: eid} = entity), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure we can actually receive our own updates, can you verify? |
||
| do: {:ok, entity} | ||
|
|
||
| def handle_event({:entity_change, %{changed: %{Position => %Position{coord: mover_coord}}, entity_id: moving_entity_id}}, | ||
| %Entity{attributes: %{Position => %Position{coord: my_coord}, | ||
| Movement => _, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we check for Movement here? I mean, if we need it for the behaviour, why not using the deconstructed variable? |
||
| Npc => %Npc{init_coord: init_coord}, | ||
| Seek => %Seek{aggro_distance: aggro_distance, escape_distance: escape_distance, target: target}}} = entity) do | ||
| case target do | ||
| nil -> | ||
| if in_aggro_range?(my_coord, mover_coord, aggro_distance) do | ||
| {:ok, entity |> seek_target_current_coord(moving_entity_id, mover_coord)} | ||
| else | ||
| {:ok, entity} | ||
| end | ||
|
|
||
| ^moving_entity_id -> | ||
| if past_escape_range?(init_coord, mover_coord, escape_distance) do | ||
| {:ok, entity |> return_to_spawn(my_coord, init_coord)} | ||
| else | ||
| {:ok, entity |> seek_target_current_coord(moving_entity_id, mover_coord)} | ||
| end | ||
|
|
||
| _ -> {:ok, entity} | ||
| end | ||
| end | ||
|
|
||
| def terminate(_reason, entity), | ||
| do: {:ok, entity |> remove_attribute(Seek)} | ||
|
|
||
| defp in_aggro_range?(my_coord, mover_coord, aggro_distance), | ||
| do: Vector.dist(my_coord, mover_coord) <= aggro_distance | ||
|
|
||
| defp past_escape_range?(init_coord, mover_coord, escape_distance), | ||
| do: Vector.dist(init_coord, mover_coord) >= escape_distance | ||
|
|
||
| defp seek_target_current_coord(%Entity{attributes: %{Position => %Position{coord: my_coord}, | ||
| Npc => %Npc{map: map}, Seek => %Seek{path: path}}} = entity, target_id, target_coord) do | ||
| {success, result} = Astar.get_path(map.nav_mesh, my_coord, target_coord) | ||
|
|
||
| %Path{vertices: new_path} = case success do | ||
| :ok -> result | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Formatting this function and the other below would be nice ;) |
||
| :error -> Path.empty | ||
| end | ||
|
|
||
| entity |> update_attribute(Seek, fn(s) -> %Seek{s | target: target_id, path: new_path} end) | ||
| end | ||
|
|
||
| defp return_to_spawn(%Entity{attributes: %{Npc => %Npc{map: map}}} = entity, my_coord, spawn_coord) do | ||
| {success, result} = Astar.get_path(map.nav_mesh, my_coord, spawn_coord) | ||
|
|
||
| %Path{vertices: new_path} = case success do | ||
| :ok -> result | ||
|
|
||
| :error -> Path.empty | ||
| end | ||
|
|
||
| entity |> update_attribute(Seek, fn(s) -> %Seek{s | target: nil, path: new_path} end) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,24 @@ | ||
| defmodule Entice.Logic.Movement do | ||
| alias Entice.Entity | ||
| alias Entice.Utils.Geom.Coord | ||
| alias Entice.Logic.{Movement, Player.Position} | ||
| alias Entice.Logic.{Movement, Player.Position, Seek} | ||
| alias Geom.Shape.{Vector, Vector2D} | ||
|
|
||
| @update_interval 50 | ||
| @speed 288 #TODO: Figure out the velocity/speed naming business | ||
| @epsilon 10 | ||
|
|
||
| @doc """ | ||
| Note that velocity is actually a coefficient for the real velocity thats used inside | ||
| the client, but for simplicities sake we used velocity as a name. | ||
| """ | ||
| defstruct goal: %Coord{}, plane: 1, move_type: 9, velocity: 1.0 | ||
| defstruct goal: %Vector2D{x: 0, y: 0}, plane: 1, move_type: 9, velocity: 1.0, auto_updating?: false | ||
|
|
||
|
|
||
| def register(entity), | ||
| do: Entity.put_behaviour(entity, Movement.Behaviour, []) | ||
|
|
||
| def register(entity, auto_updating?: auto_updating?), | ||
| do: Entity.put_behaviour(entity, Movement.Behaviour, auto_updating?: auto_updating?) | ||
|
|
||
| def unregister(entity), | ||
| do: Entity.remove_behaviour(entity, Movement.Behaviour) | ||
|
|
@@ -30,19 +35,85 @@ defmodule Entice.Logic.Movement do | |
| end) | ||
| end | ||
|
|
||
| def update_interval, do: @update_interval | ||
| def speed, do: @speed | ||
| def epsilon, do: @epsilon | ||
|
|
||
|
|
||
| defmodule Behaviour do | ||
| use Entice.Entity.Behaviour | ||
|
|
||
| def init(%Entity{attributes: %{Movement => _}} = entity, _args), | ||
| do: {:ok, entity} | ||
|
|
||
| def init(%Entity{attributes: %{Position => %Position{pos: pos, plane: plane}}} = entity, _args), | ||
| do: {:ok, entity |> put_attribute(%Movement{goal: pos, plane: plane})} | ||
| def init(%Entity{attributes: %{Position => %Position{coord: coord, plane: plane}}} = entity, auto_updating?: true) do | ||
| self |> Process.send_after(:movement_calculate_next, 1) | ||
| {:ok, entity |> put_attribute(%Movement{goal: coord, plane: plane, auto_updating?: true})} | ||
| end | ||
|
|
||
| def init(%Entity{attributes: %{Position => %Position{coord: coord, plane: plane}}} = entity, _args), | ||
| do: {:ok, entity |> put_attribute(%Movement{goal: coord, plane: plane})} | ||
|
|
||
| def init(entity, _args), | ||
| do: {:ok, entity |> put_attribute(%Movement{})} | ||
|
|
||
| #TODO: Move all this logic to seek | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea ;) |
||
| def handle_event(:movement_calculate_next, | ||
| %Entity{attributes: %{Movement => %Movement{velocity: velocity, auto_updating?: auto_updating?, goal: goal}, | ||
| Seek => %Seek{path: path}, | ||
| Position => %Position{coord: current_pos}}} = entity) do | ||
| #Determine next goal | ||
| {entity, goal} = case Enum.count(path) do | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively we can also simply match on the List like: [_ | _] -> ...
_ -> {entity, goal} |
||
| 0 -> | ||
| {entity, goal} #Empty path | ||
|
|
||
| 1 -> | ||
| {entity, goal} #Path only has starting pos left | ||
|
|
||
| _ -> | ||
| cond do | ||
| Vector.equal(goal, current_pos, Movement.epsilon) -> #Reached goal | ||
| new_goal = Enum.at(path, 1) #len path >= 2 so we know next is not nil | ||
| entity = entity | ||
| |> update_attribute(Movement, fn(m) -> %Movement{m | goal: new_goal} end) | ||
| |> update_attribute(Seek, fn(s) -> %Seek{s | path: Enum.drop(path, 1)} end) | ||
| {entity, new_goal} | ||
|
|
||
| true -> | ||
| {entity, goal} | ||
| end | ||
| end | ||
|
|
||
| #Advance pos if not at new (or unchanged) goal | ||
| next_pos = cond do | ||
| Vector.equal(goal, current_pos, Movement.epsilon) -> | ||
| current_pos | ||
|
|
||
| true -> | ||
| {:ok, next_pos} = calc_next_pos(current_pos, goal, velocity) | ||
| next_pos | ||
| end | ||
|
|
||
| entity = entity |> update_attribute(Position, fn(p) -> %Position{p | coord: next_pos} end) | ||
| if auto_updating?, do: self |> Process.send_after(:movement_calculate_next, Movement.update_interval) | ||
| {:ok, entity} | ||
| end | ||
|
|
||
| defp calc_next_pos(%Vector2D{} = current_pos, %Vector2D{} = goal, velocity) do | ||
| direction = Vector.sub(goal, current_pos) | ||
| cond do | ||
| #Convoluted cond because somehow %Vector2D{x: 0, y: 0} != %Vector2D{x: 0.0, y: 0.0} in case | ||
| Vector.equal(direction, %Vector2D{x: 0, y: 0}, 0) -> | ||
| {:ok, current_pos} | ||
|
|
||
| true -> | ||
| unit = Vector.unit(direction) | ||
| offset = Vector.mul(unit, velocity * Movement.speed * Movement.update_interval / 1000) | ||
| {:ok, Vector.add(current_pos, offset)} | ||
| end | ||
| end | ||
|
|
||
| defp calc_next_pos(_,_,_), do: {:error, :wrong_arguments} | ||
|
|
||
| def terminate(_reason, entity), | ||
| do: {:ok, entity |> remove_attribute(Movement)} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| %{"entice_entity": {:git, "https://github.com/entice/entity.git", "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a", [ref: "c26f6f77ae650e25e6cd2ffea8aae46b7d83966a"]}, | ||
| "entice_utils": {:git, "https://github.com/entice/utils.git", "79ead4dca77324b4c24f584468edbaff2029eeab", [ref: "79ead4dca77324b4c24f584468edbaff2029eeab"]}, | ||
| "inflex": {:hex, :inflex, "1.5.0"}, | ||
| "pipe": {:hex, :pipe, "0.0.2"}, | ||
| "uuid": {:hex, :uuid, "1.1.3"}} | ||
| "geom": {:hex, :geom, "1.0.0", "b95356b34025b71096db92a6c3be47e7729db6983223a6d03cdff85aec01c3f5", [:mix], [{:poison, "~> 2.0 or ~>3.0", [hex: :poison, optional: false]}]}, | ||
| "inflex": {:hex, :inflex, "1.5.0", "e4ff5d900280b2011b24d1ac1c4590986ee5add2ea644c9894e72213cf93ff0b", [:mix], []}, | ||
| "pipe": {:hex, :pipe, "0.0.2", "eff98a868b426745acef103081581093ff5c1b88100f8ff5949b4a30e81d0d9f", [:mix], []}, | ||
| "poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], []}, | ||
| "uuid": {:hex, :uuid, "1.1.3", "06ca38801a1a95b751701ca40716bb97ddf76dfe7e26da0eec7dba636740d57a", [:mix], []}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens with this behaviour if no params is given? Does it even work then?