-
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 4 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,69 @@ | ||
| defmodule Entice.Logic.Seek do | ||
| alias Entice.Entity | ||
| alias Entice.Logic.{Seek, Player.Position, Npc, Movement} | ||
| alias Entice.Utils.Geom.Coord | ||
|
|
||
| defstruct target: nil, aggro_distance: 10, escape_distance: 20 | ||
|
|
||
| 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({:movement_agent_updated, %Position{pos: _}, moving_entity_id}, %Entity{id: my_id} = entity) | ||
| when moving_entity_id == my_id, | ||
| do: {:ok, entity} | ||
|
|
||
| def handle_event({:movement_agent_updated, %Position{pos: mover_pos}, moving_entity_id}, | ||
| %Entity{attributes: %{Position => %Position{pos: my_pos}, | ||
| 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_pos: init_pos}, | ||
| Seek => %Seek{aggro_distance: aggro_distance, escape_distance: escape_distance, target: target}}} = entity) do | ||
| case target do | ||
| nil -> | ||
| if calc_distance(my_pos, mover_pos) < aggro_distance do | ||
| {:ok, entity |> update_attribute(Seek, fn(s) -> %Seek{s | target: moving_entity_id} end) | ||
| |> update_attribute(Movement, fn(m) -> %Movement{m | goal: mover_pos} end)} | ||
| else | ||
| {:ok, entity} | ||
| end | ||
|
|
||
| ^moving_entity_id -> | ||
| if calc_distance(init_pos, mover_pos) >= escape_distance do | ||
| {:ok, entity | ||
| |> update_attribute(Seek, fn(s) -> %Seek{s | target: nil} end) | ||
| |> update_attribute(Movement, fn(m) -> %Movement{m | goal: init_pos} end)} | ||
| else | ||
| {:ok, entity | ||
| |> update_attribute(Movement, fn(m) -> %Movement{m | goal: mover_pos} end)} | ||
| end | ||
|
|
||
| _ -> {:ok, entity} | ||
| end | ||
| end | ||
|
|
||
| def terminate(_reason, entity), | ||
| do: {:ok, entity |> remove_attribute(Seek)} | ||
|
|
||
| #TODO: Should probably move to Coord in Utils | ||
| defp calc_distance(%Coord{x: x1, y: y1}, %Coord{x: x2, y: y2}) do | ||
| :math.sqrt(:math.pow((x2-x1), 2) + :math.pow((y2-y1), 2)) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| defmodule Entice.Logic.Movement do | ||
| alias Entice.Entity | ||
| alias Entice.Entity.Coordination | ||
| alias Entice.Utils.Geom.Coord | ||
| alias Entice.Logic.{Movement, Player.Position} | ||
| alias Entice.Logic.{Movement, MapInstance, Player.Position} | ||
|
|
||
|
|
||
| @doc """ | ||
|
|
@@ -28,6 +29,9 @@ defmodule Entice.Logic.Movement do | |
| |> Map.put(Position, new_pos) | ||
| |> Map.put(Movement, new_movement) | ||
| end) | ||
| {:ok, %MapInstance{map: map, players: _}} = Entity.fetch_attribute(entity, MapInstance) | ||
|
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. Does the player have a map-instance attribute? I thought it was for maps only...
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 added it to be able to notify the player's map's agents but I'll remove it now that we're gonna use :entity_change instead. |
||
| #Can't figure out how to get eid from pid | ||
| Coordination.notify_all(map, {:movement_agent_updated, new_pos, entity}) #TODO: Change to use eid instead of pid for self seeking guard in seek l30 | ||
|
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. Do we need this? Or can you maybe match on state changes later on like in https://github.com/entice/web/blob/develop/web/channels/entity_channel.ex#L74-L78
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. Aha! This is what I was looking for after our talk the other day but all I had found was handle_change which wasn't appropriate for this case. This will simplify stuff. |
||
| end | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,19 @@ | ||
| defmodule Entice.Logic.Npc do | ||
| use Entice.Logic.Map | ||
| alias Entice.Entity | ||
| alias Entice.Logic.{Npc, Vitals} | ||
| alias Entice.Logic.{Npc, Vitals, Movement, Seek} | ||
| alias Entice.Logic.Player.{Name, Position, Level} | ||
|
|
||
| defstruct npc_model_id: :dhuum, init_pos: %Position{} | ||
|
|
||
| defstruct(npc_model_id: :dhuum) | ||
|
|
||
|
|
||
| def spawn(name, model, %Position{} = position) | ||
| def spawn(name, model, %Position{} = position, %{seeks: seeks} \\ %{seeks: true}) | ||
|
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 guessing you'd want to have an arbitrary listing of named option, which is what you can use a keyword list for like this: # In the function
def some_func(arg1, arg2, opts \\ []) do
some_opt = opts[:some_opt] || 1337 # Either takes the value from opts or a default
# ...
# Later on at caller site:
some_func(1, 2, some_opt: :cool, another_thing: 42) # Can take arbitrarily long options
|
||
| when is_binary(name) and is_atom(model) do | ||
| {:ok, id, pid} = Entity.start() | ||
| Npc.register(id, name, model, position) | ||
| Vitals.register(id) | ||
| Movement.register(id) | ||
| if seeks, do: Seek.register(id) | ||
| {:ok, id, pid} | ||
| end | ||
|
|
||
|
|
@@ -23,7 +24,7 @@ defmodule Entice.Logic.Npc do | |
| attrs | ||
| |> Map.put(Name, %Name{name: name}) | ||
| |> Map.put(Position, position) | ||
| |> Map.put(Npc, %Npc{npc_model_id: model}) | ||
| |> Map.put(Npc, %Npc{npc_model_id: model, init_pos: position.pos}) | ||
| |> Map.put(Level, %Level{level: 20}) | ||
| end) | ||
| end | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ defmodule Entice.Logic.Player do | |
| defmodule Name, do: defstruct( | ||
| name: "Unknown Entity") | ||
|
|
||
| #TODO: rename :pos to :coord ? Confusing sometimes | ||
|
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. Sounds like a good idea! |
||
| defmodule Position, do: defstruct( | ||
| pos: %Coord{}, | ||
| plane: 1) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| defmodule Entice.Logic.SeekTest do | ||
| use ExUnit.Case, async: true | ||
| use Entice.Logic.Maps | ||
| use Entice.Logic.Attributes | ||
| alias Entice.Entity | ||
| alias Entice.Logic.{Player, Seek} | ||
| alias Entice.Logic.Player.Position | ||
| alias Entice.Entity.Coordination | ||
|
|
||
|
|
||
| setup do | ||
| {:ok, npc_eid, npc_pid} = Npc.spawn("Dhuum", :dhuum, %Position{pos: %Coord{x: 10, y: 10}}) | ||
| {:ok, player_eid, player_pid} = Entity.start | ||
| Player.register(player_pid, HeroesAscent) | ||
| {:ok, [npc_entity: npc_pid, npc_eid: npc_eid, player_eid: player_eid, player_entity: player_pid]} | ||
| end | ||
|
|
||
| test "correct default register", %{npc_entity: pid} do | ||
| assert {:ok, %Seek{target: nil, aggro_distance: 10, escape_distance: 20}} = Entity.fetch_attribute(pid, Seek) | ||
| end | ||
|
|
||
| test "update same entity", %{npc_entity: pid, npc_eid: eid} do | ||
| Coordination.notify(pid, {:movement_agent_updated, %Position{pos: %Coord{x: 1, y: 2}}, eid}) | ||
| assert {:ok, %Seek{target: nil}} = Entity.fetch_attribute(pid, Seek) | ||
| end | ||
|
|
||
| test "update no target, entity close enough to aggro", %{npc_entity: npc_pid, player_eid: player_eid} do | ||
| #Move player within 2 units of distance of npc with aggro distance of 10 (default) | ||
| mover_pos = %Coord{x: 14, y: 10} | ||
| Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: mover_pos}, player_eid}) | ||
|
|
||
| assert {:ok, %Seek{target: ^player_eid}} = Entity.fetch_attribute(npc_pid, Seek) | ||
| assert {:ok, %Movement{goal: ^mover_pos}} = Entity.fetch_attribute(npc_pid, Movement) | ||
| end | ||
|
|
||
| test "update no target, entity too far to aggro", %{npc_entity: npc_pid, player_eid: player_eid} do | ||
| Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: %Coord{x: 19, y: 15}}, player_eid}) | ||
| assert {:ok, %Seek{target: nil}} = Entity.fetch_attribute(npc_pid, Seek) | ||
| end | ||
|
|
||
| test "update has target, entity is not current target", %{npc_entity: npc_pid, player_eid: player_eid} do | ||
| #Set player as target | ||
| Entity.put_attribute(npc_pid, %Seek{target: player_eid, aggro_distance: 10, escape_distance: 20}) | ||
|
|
||
| #Create unrelated entity | ||
| {:ok, unknown_eid, _} = Entity.start | ||
|
|
||
| Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: %Coord{x: 11, y: 11}}, unknown_eid}) | ||
| assert {:ok, %Seek{target: ^player_eid}} = Entity.fetch_attribute(npc_pid, Seek) | ||
| end | ||
|
|
||
| test "update has target, entity is current target, entity escapes", %{npc_entity: npc_pid, player_eid: player_eid} do | ||
| init_pos = %Coord{x: 10, y: 10} | ||
|
|
||
| #Set player as target | ||
| Entity.put_attribute(npc_pid, %Seek{target: player_eid, aggro_distance: 10, escape_distance: 20}) | ||
|
|
||
| #Notify of new position outside escape range (dist = 20.6 > 20) | ||
| Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: %Coord{x: 30, y: 15}}, player_eid}) | ||
| assert {:ok, %Seek{target: nil}} = Entity.fetch_attribute(npc_pid, Seek) | ||
| assert {:ok, %Movement{goal: ^init_pos}} = Entity.fetch_attribute(npc_pid, Movement) | ||
| end | ||
|
|
||
| test "update has target, entity is current target, entity does not escape", %{npc_entity: npc_pid, player_eid: player_eid} do | ||
| #Set player as target | ||
| Entity.put_attribute(npc_pid, %Seek{target: player_eid, aggro_distance: 10, escape_distance: 20}) | ||
|
|
||
| #Notify of new position outside escape range (dist = 20.6 > 20) | ||
| new_player_pos = %Coord{x: 14, y: 15} | ||
| Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: new_player_pos}, player_eid}) | ||
| assert {:ok, %Seek{target: ^player_eid}} = Entity.fetch_attribute(npc_pid, Seek) | ||
| assert {:ok, %Movement{goal: ^new_player_pos}} = Entity.fetch_attribute(npc_pid, Movement) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| defmodule Entice.Logic.MovementTest do | ||
| use ExUnit.Case, async: true | ||
| alias Entice.Entity | ||
| alias Entice.Entity.Coordination | ||
| alias Entice.Utils.Geom.Coord | ||
| alias Entice.Logic.Movement | ||
| alias Entice.Logic.{Movement, MapInstance} | ||
| alias Entice.Logic.Player.Position | ||
| use Entice.Logic.Map | ||
|
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.
|
||
|
|
||
| defmap TestMap1 | ||
|
|
||
| setup do | ||
| {:ok, _id, pid} = Entity.start | ||
|
|
@@ -38,11 +41,16 @@ defmodule Entice.Logic.MovementTest do | |
|
|
||
|
|
||
| test "update", %{entity: pid} do | ||
| Entity.put_attribute(pid, %MapInstance{map: TestMap1}) | ||
| Coordination.register_observer(self, TestMap1) | ||
|
|
||
| new_pos = %Position{pos: %Coord{x: 42, y: 1337}, plane: 7} | ||
| Movement.update(pid, | ||
| %Position{pos: %Coord{x: 42, y: 1337}, plane: 7}, | ||
| new_pos, | ||
| %Movement{goal: %Coord{x: 1337, y: 42}, plane: 13, move_type: 5, velocity: 0.5}) | ||
| assert {:ok, %Position{plane: 7}} = Entity.fetch_attribute(pid, Position) | ||
| assert {:ok, %Movement{move_type: 5}} = Entity.fetch_attribute(pid, Movement) | ||
| assert {:ok, %Movement{move_type: 5}} = Entity.fetch_attribute(pid, Movement) | ||
| assert_receive {:movement_agent_updated, ^new_pos, ^pid} #TODO: update to use eid | ||
| end | ||
|
|
||
|
|
||
|
|
||
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?