Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3d72012
Skeleton
EtienneDesticourt Aug 8, 2016
e9913c3
Seek logic rework, removed channel callback
EtienneDesticourt Aug 9, 2016
59fa130
Fixed seek logic and added tests, fixed movement update, added ref to…
EtienneDesticourt Aug 10, 2016
3792cbd
Minor rename for clarity
EtienneDesticourt Aug 10, 2016
1fd1a23
Modified Seek to listen for :entity_change on Position
EtienneDesticourt Aug 10, 2016
239f2cc
:pos -> :coord
EtienneDesticourt Aug 10, 2016
165397c
Modified way AI behaviours are chosen for an npc
EtienneDesticourt Aug 10, 2016
f382506
Removed added whitespace
EtienneDesticourt Aug 10, 2016
e9c9693
Merge remote-tracking branch 'refs/remotes/origin/master' into AI_Pro…
EtienneDesticourt Aug 10, 2016
6fdee5b
:pos -> :coord
EtienneDesticourt Aug 10, 2016
f15a24f
Whitespace removal
EtienneDesticourt Aug 11, 2016
b2a2252
Added auto pos update skeleton
EtienneDesticourt Aug 11, 2016
12af7b6
Cleaned up seeking handler, fixed movement update handler
EtienneDesticourt Aug 15, 2016
260666f
Removed excess linebreak
EtienneDesticourt Aug 15, 2016
f718ddf
Naming changes
EtienneDesticourt Aug 16, 2016
38b65ca
Updated deps
EtienneDesticourt Oct 10, 2016
f819c1d
Put better default aggro/escape dist
EtienneDesticourt Oct 10, 2016
f7ab019
Set seeking as default npc behaviour, started auto updates at behavio…
EtienneDesticourt Oct 10, 2016
0c18581
Fixed tests
EtienneDesticourt Oct 10, 2016
d93eeb8
Changed event from map to atom, fixed seeks option
EtienneDesticourt Oct 11, 2016
e6dc0a6
Added geom deps, replaced Coord with Vector2D, added NavMesh to maps
EtienneDesticourt Oct 14, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions lib/entice/logic/ai/seek.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,35 @@ defmodule Entice.Logic.Seek do
def init(entity, %{aggro_distance: aggro_distance, escape_distance: escape_distance}),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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),
def init(entity, _args),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace change - kittens gonna die! :'(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace at end of line ;)

do: {:ok, entity |> put_attribute(%Seek{})}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, no args = what behaviour?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right I'll add an init that only take an entity

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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,
def handle_event({:entity_change, %{entity_id: eid}}, %Entity{id: eid} = entity),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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({:movement_agent_updated, %Position{pos: mover_pos}, moving_entity_id},
%Entity{attributes: %{Position => %Position{pos: my_pos},
def handle_event({:entity_change, %{changed: %{Position => %Position{coord: mover_coord}}, entity_id: moving_entity_id}},
%Entity{attributes: %{Position => %Position{coord: my_coord},
Movement => _,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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},
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 calc_distance(my_pos, mover_pos) < aggro_distance do
if calc_distance(my_coord, mover_coord) < 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)}
|> update_attribute(Movement, fn(m) -> %Movement{m | goal: mover_coord} end)}
else
{:ok, entity}
end

^moving_entity_id ->
if calc_distance(init_pos, mover_pos) >= escape_distance do
if calc_distance(init_coord, mover_coord) >= 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)}
|> update_attribute(Movement, fn(m) -> %Movement{m | goal: init_coord} end)}
else
{:ok, entity
|> update_attribute(Movement, fn(m) -> %Movement{m | goal: mover_pos} end)}
|> update_attribute(Movement, fn(m) -> %Movement{m | goal: mover_coord} end)}
end

_ -> {:ok, entity}
Expand Down
2 changes: 0 additions & 2 deletions lib/entice/logic/map_instance.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ defmodule Entice.Logic.MapInstance do
def handle_event(
{:map_instance_player_add, player_entity},
%Entity{attributes: %{MapInstance => %MapInstance{map: map, players: players}}} = entity) do
player_entity |> Entity.attribute_transaction(fn attrs -> attrs |> Map.put(MapInstance, %MapInstance{map: map}) end)
Coordination.register(player_entity, map) # TODO change map to something else if we have multiple instances
{:ok, entity |> update_attribute(MapInstance, fn(m) -> %MapInstance{m | players: players+1} end)}
end
Expand All @@ -53,7 +52,6 @@ defmodule Entice.Logic.MapInstance do
{:map_instance_npc_add, %{name: name, model: model, position: position}},
%Entity{attributes: %{MapInstance => %MapInstance{map: map}}} = entity) do
{:ok, eid, _pid} = Npc.spawn(name, model, position)
eid |> Entity.attribute_transaction(fn attrs -> attrs |> Map.put(MapInstance, %MapInstance{map: map}) end)
Coordination.register(eid, map) # TODO change map to something else if we have multiple instances
{:ok, entity}
end
Expand Down
10 changes: 3 additions & 7 deletions lib/entice/logic/movement.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
defmodule Entice.Logic.Movement do
alias Entice.Entity
alias Entice.Entity.Coordination
alias Entice.Utils.Geom.Coord
alias Entice.Logic.{Movement, MapInstance, Player.Position}
alias Entice.Logic.{Movement, Player.Position}


@doc """
Expand All @@ -29,9 +28,6 @@ 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)
#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
end


Expand All @@ -41,8 +37,8 @@ defmodule Entice.Logic.Movement do
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, _args),
do: {:ok, entity |> put_attribute(%Movement{goal: coord, plane: plane})}

def init(entity, _args),
do: {:ok, entity |> put_attribute(%Movement{})}
Expand Down
8 changes: 4 additions & 4 deletions lib/entice/logic/npc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ defmodule Entice.Logic.Npc do
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, init_coord: %Position{}


def spawn(name, model, %Position{} = position, %{seeks: seeks} \\ %{seeks: true})
def spawn(name, model, %Position{} = position, opts \\ [])
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)
if opts[:seeks] || true, do: Seek.register(id)
{:ok, id, pid}
end

Expand All @@ -24,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, init_pos: position.pos})
|> Map.put(Npc, %Npc{npc_model_id: model, init_coord: position.coord})
|> Map.put(Level, %Level{level: 20})
end)
end
Expand Down
5 changes: 2 additions & 3 deletions lib/entice/logic/player.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ defmodule Entice.Logic.Player do
defmodule Name, do: defstruct(
name: "Unknown Entity")

#TODO: rename :pos to :coord ? Confusing sometimes
defmodule Position, do: defstruct(
pos: %Coord{},
coord: %Coord{},
plane: 1)

defmodule Appearance, do: defstruct(
Expand All @@ -32,7 +31,7 @@ defmodule Entice.Logic.Player do
entity |> Entity.attribute_transaction(fn (attrs) ->
attrs
|> Map.put(Name, %Name{name: name})
|> Map.put(Position, %Position{pos: map.spawn})
|> Map.put(Position, %Position{coord: map.spawn})
|> Map.put(Appearance, appearance)
|> Map.put(Level, %Level{level: 20})
end)
Expand Down
71 changes: 46 additions & 25 deletions test/entice/logic/ai/seek_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,69 +6,90 @@ defmodule Entice.Logic.SeekTest do
alias Entice.Logic.{Player, Seek}
alias Entice.Logic.Player.Position
alias Entice.Entity.Coordination
use Entice.Logic.Map

defmap TestMap

setup do
{:ok, npc_eid, npc_pid} = Npc.spawn("Dhuum", :dhuum, %Position{pos: %Coord{x: 10, y: 10}})
npc_init_coord = %Coord{x: 10, y: 10}
{:ok, npc_eid, npc_pid} = Npc.spawn("Dhuum", :dhuum, %Position{coord: npc_init_coord})
{: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]}
#Sets an initial pos for the player so the Position attr appears as changed and not added in the following tests
simulate_movement_update(player_pid, %Position{coord: %Coord{x: 1, y: 2}}, %Movement{goal: %Coord{x: 3, y: 4}})

Player.register(player_pid, TestMap)
Coordination.register(player_eid, TestMap)
Coordination.register(npc_eid, TestMap)
{:ok, [npc_entity: npc_pid, npc_eid: npc_eid, player_eid: player_eid, player_entity: player_pid, npc_init_coord: npc_init_coord]}
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})
test "update same entity", %{npc_entity: pid} do
simulate_movement_update(pid, %Position{coord: %Coord{x: 1, y: 2}}, %Movement{goal: %Coord{x: 3, y: 4}})
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
test "update no target, entity close enough to aggro", %{npc_entity: npc_pid, player_eid: player_eid, player_entity: player_pid} 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})
mover_coord = %Coord{x: 14, y: 10}
simulate_movement_update(player_pid, %Position{coord: mover_coord}, %Movement{goal: mover_coord})


assert {:ok, %Seek{target: ^player_eid}} = Entity.fetch_attribute(npc_pid, Seek)
assert {:ok, %Movement{goal: ^mover_pos}} = Entity.fetch_attribute(npc_pid, Movement)
assert {:ok, %Movement{goal: ^mover_coord}} = 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})
test "update no target, entity too far to aggro", %{npc_entity: npc_pid, player_entity: player_pid} do
simulate_movement_update(player_pid, %Position{coord: %Coord{x: 19, y: 15}}, %Movement{goal: %Coord{x: 0, y: 0}})
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
#Create unrelated entity at a random pos and add it to map channel
{:ok, other_player_eid, other_player_pid} = Entity.start
simulate_movement_update(other_player_pid, %Position{coord: %Coord{x: 1, y: 2}}, %Movement{goal: %Coord{x: 3, y: 4}})
Coordination.register(other_player_eid, TestMap)

Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: %Coord{x: 11, y: 11}}, unknown_eid})
#Move other player inside of aggro range and check that npc keeps initial target
simulate_movement_update(other_player_pid, %Position{coord: %Coord{x: 11, y: 11}}, %Movement{goal: %Coord{x: 0, y: 0}})
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}

test "update has target, entity is current target, entity escapes",
%{npc_entity: npc_pid, player_eid: player_eid, player_entity: player_pid, npc_init_coord: npc_init_coord} 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)
Coordination.notify(npc_pid, {:movement_agent_updated, %Position{pos: %Coord{x: 30, y: 15}}, player_eid})
#Move target player far enough to escape
simulate_movement_update(player_pid, %Position{coord: %Coord{x: 30, y: 15}}, %Movement{goal: %Coord{x: 0, y: 0}})
assert {:ok, %Seek{target: nil}} = Entity.fetch_attribute(npc_pid, Seek)
assert {:ok, %Movement{goal: ^init_pos}} = Entity.fetch_attribute(npc_pid, Movement)
assert {:ok, %Movement{goal: ^npc_init_coord}} = 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
test "update has target, entity is current target, entity does not escape", %{npc_entity: npc_pid, player_eid: player_eid, player_entity: player_pid} 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})
#Move target player but not far enough to escape
new_player_coord = %Coord{x: 14, y: 15}
simulate_movement_update(player_pid, %Position{coord: new_player_coord}, %Movement{goal: %Coord{x: 0, y: 0}})
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)
assert {:ok, %Movement{goal: ^new_player_coord}} = Entity.fetch_attribute(npc_pid, Movement)
end


defp simulate_movement_update(entity_pid, new_coord, new_movement) do
entity_pid |> Entity.attribute_transaction(
fn attrs ->
attrs
|> Map.put(Position, new_coord)
|> Map.put(Movement, new_movement)
end)
end
end
16 changes: 4 additions & 12 deletions test/entice/logic/movement_test.exs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
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, MapInstance}
alias Entice.Logic.Movement
alias Entice.Logic.Player.Position
use Entice.Logic.Map

defmap TestMap1

setup do
{:ok, _id, pid} = Entity.start
Expand All @@ -24,7 +21,7 @@ defmodule Entice.Logic.MovementTest do

test "register with position", %{entity: pid} do
Movement.unregister(pid) # remove again, so we can add a new one
Entity.put_attribute(pid, %Position{pos: %Coord{x: 42, y: 1337}, plane: 7})
Entity.put_attribute(pid, %Position{coord: %Coord{x: 42, y: 1337}, plane: 7})
Movement.register(pid)
m = %Movement{goal: %Coord{x: 42, y: 1337}, plane: 7}
assert {:ok, ^m} = Entity.fetch_attribute(pid, Movement)
Expand All @@ -41,16 +38,11 @@ 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,
new_pos,
%Position{coord: %Coord{x: 42, y: 1337}, plane: 7},
%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_receive {:movement_agent_updated, ^new_pos, ^pid} #TODO: update to use eid
assert {:ok, %Movement{move_type: 5}} = Entity.fetch_attribute(pid, Movement)
end


Expand Down