-
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 12 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,68 @@ | ||
| 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({: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 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_coord} end)} | ||
| else | ||
| {:ok, entity} | ||
| end | ||
|
|
||
| ^moving_entity_id -> | ||
| 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_coord} end)} | ||
| else | ||
| {:ok, entity | ||
| |> update_attribute(Movement, fn(m) -> %Movement{m | goal: mover_coord} 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 |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ defmodule Entice.Logic.Movement do | |
| 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: %Coord{}, plane: 1, move_type: 9, velocity: 1.0, update_self: false, update_delay: 1 | ||
|
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 was thinking maybe we can shorten this a bit - for starters, I guess the update interval is static and can be compile with
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. How about auto_move_update ? |
||
|
|
||
|
|
||
| def register(entity), | ||
|
|
@@ -37,14 +37,28 @@ 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{})} | ||
|
|
||
| def handle_event({:movement_calculate_next}, | ||
| %Entity{attributes: %{Movement => %Movement{update_self: update_self, update_delay: update_delay}}} = _entity) do | ||
| #TODO: implement once the whole collision business is handled | ||
| if update_self, do: start_update_trigger_timer(:movement_calculate_next, update_delay) | ||
|
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 need to return |
||
| end | ||
|
|
||
| def terminate(_reason, entity), | ||
| do: {:ok, entity |> remove_attribute(Movement)} | ||
|
|
||
| defp start_update_trigger_timer(message, time) 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. Is there a need for this check? Otherwise just simply always use Process.send_after for simplicity
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 don't know. I copied it from here 20aa29d#diff-366987ce92dde211ad3e5f552c55e0d4R148 and assumed there was some kind of problem with send_after(message, 0). Although looking at it again it seems it might have just been to return nil so the calling function could interpret it as a timer ref so I'll replace it as you said.
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. In the other case we needed either the |
||
| if time == 0 do | ||
| self |> send(message) | ||
| nil | ||
| else | ||
| self |> Process.send_after(message, time) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| 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 | ||
| use Entice.Logic.Map | ||
|
|
||
| defmap TestMap | ||
|
|
||
| setup do | ||
| 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 | ||
| #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} 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, player_entity: player_pid} do | ||
| #Move player within 2 units of distance of npc with aggro distance of 10 (default) | ||
| 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_coord}} = Entity.fetch_attribute(npc_pid, Movement) | ||
| end | ||
|
|
||
| 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 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) | ||
|
|
||
| #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, 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}) | ||
|
|
||
| #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: ^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, player_entity: player_pid} do | ||
| #Set player as target | ||
| Entity.put_attribute(npc_pid, %Seek{target: player_eid, aggro_distance: 10, escape_distance: 20}) | ||
|
|
||
| #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_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 |
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?