--- title: selector tags: content, MCEA, technology --- # 實體選擇器(Selector) 1.19 \net\minecraft\command\arguments\selector\EntitySelector.java \net\minecraft\command\arguments\selector\EntitySelectorParser.java Parameters ```java int limit; // number limit boolean includeNonPlayers; // type!=player boolean currentWorldOnly; // current world only Predicate<Entity> filter; // predicates MinMaxBounds.FloatBound distance; // a pair number of distance range Function<Vector3d, Vector3d> positionGetter; // AxisAlignedBB aabb; // align axis BiConsumer<Vector3d, List<? extends Entity>> sorter; boolean self; // @s String username; // name UUID uuid; // uuid EntityType<?> type; // type boolean checkPermission; ``` > 實體選擇器是一個效率非常慢的功能,在大多情況應當盡可能避免使用實體選擇器 ### Entity Selector Parser (實體選擇器解析器) ![](https://i.imgur.com/hEgKBmT.png =400x) **Entity Selector Parser(ESP)** -> **Entity Selector(ES)** (此簡寫非正式縮寫,僅用於在本文中可以更方便的表示所用) `StringReader reader` : 被分析的選擇器 :::spoiler parser() ```java= public EntitySelector parse() throws CommandSyntaxException { this.startPosition = this.reader.getCursor(); this.suggestions = this::suggestNameOrSelector; if (this.reader.canRead() && this.reader.peek() == '@') { if (!this.allowSelectors) { throw ERROR_SELECTORS_NOT_ALLOWED.createWithContext(this.reader); } this.reader.skip(); EntitySelector forgeSelector = net.minecraftforge.common.command.EntitySelectorManager.parseSelector(this); if (forgeSelector != null) return forgeSelector; this.parseSelector(); } else { this.parseNameOrUUID(); } this.finalizePredicates(); return this.getSelector(); } ``` ::: > ESP的功能主要是構建ES,並將ES產生之List傳遞給指令使用 我們會用以下的集合代號來描述群體: $E:$ 實體的集合 $P:$ 玩家的集合 對於五大選擇器,其分類是這樣子的: | Selector | Description & Complexity | | -------- | -------- | | @s | 掃描自己本身,時間複雜度為 $O(1)$ | | @e | 掃描所有實體,時間複雜度為 $O(\|E\|)$ | | @a | 掃描所有玩家,時間複雜度為 $O(\|P\|)$,且$P\subseteq E$ | | @r | 掃描所有玩家並隨機排序,時間複雜度為 $O(\|P\|)$,且$P\subseteq E$ | | @p | 掃描所有玩家並最近排序,時間複雜度為 $O(\|P\|\log\|P\|)$,且$P\subseteq E$ | Name與UUID的解析過程: ![](https://i.imgur.com/TPW9XYE.png) 選擇器會優先判斷輸入的字串是否是以`@`為起頭, 若為否會檢查UUID的格式,若UUID格式未通過則會判定為User name 檢查完@字符後會再確認是否以`[`為接續,若為是則之後的文字皆為selector options的內容 --- ### Entity Selector ![](https://i.imgur.com/y495NzD.png) 起始呼叫情形一共有四種: :::spoiler **findSingleEntity:** 用於選擇單一實體的 ```java= public Entity findSingleEntity(CommandSourceStack p_121140_) throws CommandSyntaxException { this.checkPermissions(p_121140_); List<? extends Entity> list = this.findEntities(p_121140_); if (list.isEmpty()) { throw EntityArgument.NO_ENTITIES_FOUND.create(); } else if (list.size() > 1) { throw EntityArgument.ERROR_NOT_SINGLE_ENTITY.create(); } else { return list.get(0); } } ``` ::: :::spoiler **findEntities:** 用於選擇數個實體 ```java= public List<? extends Entity> findEntities(CommandSourceStack p_121161_) throws CommandSyntaxException { this.checkPermissions(p_121161_); if (!this.includesEntities) { return this.findPlayers(p_121161_); } else if (this.playerName != null) { ServerPlayer serverplayer = p_121161_.getServer().getPlayerList().getPlayerByName(this.playerName); return (List<? extends Entity>)(serverplayer == null ? Collections.emptyList() : Lists.newArrayList(serverplayer)); } else if (this.entityUUID != null) { for(ServerLevel serverlevel1 : p_121161_.getServer().getAllLevels()) { Entity entity = serverlevel1.getEntity(this.entityUUID); if (entity != null) { return Lists.newArrayList(entity); } } return Collections.emptyList(); } else { Vec3 vec3 = this.position.apply(p_121161_.getPosition()); Predicate<Entity> predicate = this.getPredicate(vec3); if (this.currentEntity) { return (List<? extends Entity>)(p_121161_.getEntity() != null && predicate.test(p_121161_.getEntity()) ? Lists.newArrayList(p_121161_.getEntity()) : Collections.emptyList()); } else { List<Entity> list = Lists.newArrayList(); if (this.isWorldLimited()) { this.addEntities(list, p_121161_.getLevel(), vec3, predicate); } else { for(ServerLevel serverlevel : p_121161_.getServer().getAllLevels()) { this.addEntities(list, serverlevel, vec3, predicate); } } return this.sortAndLimit(vec3, list); } } } ``` ::: :::spoiler **findSinglePlayer:** 用於選擇單一玩家 ```java= public ServerPlayer findSinglePlayer(CommandSourceStack p_121164_) throws CommandSyntaxException { this.checkPermissions(p_121164_); List<ServerPlayer> list = this.findPlayers(p_121164_); if (list.size() != 1) { throw EntityArgument.NO_PLAYERS_FOUND.create(); } else { return list.get(0); } } ``` ::: :::spoiler **findPlayers:** 用於選擇數個玩家 ```java= public List<ServerPlayer> findPlayers(CommandSourceStack p_121167_) throws CommandSyntaxException { this.checkPermissions(p_121167_); if (this.playerName != null) { ServerPlayer serverplayer2 = p_121167_.getServer().getPlayerList().getPlayerByName(this.playerName); return (List<ServerPlayer>)(serverplayer2 == null ? Collections.emptyList() : Lists.newArrayList(serverplayer2)); } else if (this.entityUUID != null) { ServerPlayer serverplayer1 = p_121167_.getServer().getPlayerList().getPlayer(this.entityUUID); return (List<ServerPlayer>)(serverplayer1 == null ? Collections.emptyList() : Lists.newArrayList(serverplayer1)); } else { Vec3 vec3 = this.position.apply(p_121167_.getPosition()); Predicate<Entity> predicate = this.getPredicate(vec3); if (this.currentEntity) { if (p_121167_.getEntity() instanceof ServerPlayer) { ServerPlayer serverplayer3 = (ServerPlayer)p_121167_.getEntity(); if (predicate.test(serverplayer3)) { return Lists.newArrayList(serverplayer3); } } return Collections.emptyList(); } else { List<ServerPlayer> list; if (this.isWorldLimited()) { list = p_121167_.getLevel().getPlayers(predicate); } else { list = Lists.newArrayList(); for(ServerPlayer serverplayer : p_121167_.getServer().getPlayerList().getPlayers()) { if (predicate.test(serverplayer)) { list.add(serverplayer); } } } return this.sortAndLimit(vec3, list); } } } ``` ::: > 在經過上述結構圖之後,他們會再藉由這四種情形建立滿足條件的實體: 他們彼此間的傳遞關係即如上述結構圖所示, 若**select**抓到的實體皆是玩家時,會改呼叫**selectPlayers**, 對於**select**和**selectPlayers**都會先判斷輸入值是否為username或uuid, 但這邊也請特別留意,搜尋username的方式是使用窮舉玩家的方式來跑, 只有uuid的搜尋,也就是4種中只有getEntityByUuid的時間為$O(1)$, 綜合並上述結果,基本上可以大致排序出一個純選擇器的效能消耗順序為: `UUID < @s < @a = @r < @p < @e` > UUID和@s相比基本上是差不多的,不過@s會多處理很多傳遞實體的東西, > 相比於只需要計算Hash值的UUID,@s很容易就會消耗很多額外的效能 --- ### 選擇器的執行順序 ###### (ref. SelectorParser.findPlayers) ``` 如果是Name 則回傳符合Name的資料 如果是UUID 則回傳符合UUID的實體資料 如果選擇器是@s 則回傳來源實體,如果來源實體符合選擇器條件(selector predicate) 否則 記錄每一個符合選擇器條件的實體到List中 將此List依照條件進行排序 回傳此List取出指定數量(limit)的部分 ``` 實體會根據條件先被篩選出來並建立成一個新的List,再將其排序並篩選數量, 這也就導致兩個問題: <font color=red>1. 只要有進行sort,就一定會拖慢selector的效能,因為O(nlogn)恆大於O(n) 2. limit並不會加快selector的效能,即使是limit=1</font> --- ### sort 功能用於指定實體排序的方式,sort選項一共有4種: * arbitrary: 預設的排序方式 (依照出生時間先後做排序) * nearest: 最近排序,第一個為最近實體、最後一個為最遠實體 * furtherest: 最遠排序,第一個為最遠實體、最後一個為最近實體 * random: 隨機排序,使用Collection.Shuffle,時間為 $O(N)$ > 比較距離的排序法:合併排序(Merge Sort),時間為 $O(N\log N)$ > 比較距離時會進行開根號的動作 --- ### 範例、取得相同分數的實體集合 #### ──────〔Version 1〕分數相等判別法 ────── ``` # copy id for matching : O(E) execute as @e[scores={id=1..}] run scoreboard players operation @s match_id = @s id execute as @e[score={match_id=1..}] run function namespace:match ``` ``` # namespace:match.mcfunction tag @s add matched # match target : O(E^2) execute as @e[tag=!matched] if score @s id = @e[tag=matched,limit=1] id run tag @s add matched scoreboard players reset @e[tag=matched] match_id tag @e remove matched ``` 時間複雜度為 $O(E^3)$ 這個寫法乍看上去比較簡約,但是在效能上並不是最優解 因為在match target的步驟中,會需要使用 $O(E^2)$ 的時間把相等分數的實體都給賦予標籤 #### ──────〔Version 2〕分數遞次篩選法 ────── ``` # space:match.mcfunction scoreboard players set 0-0-0-0-1 id 0 # copy id for matching : O(E) execute as @e[scores={id=1..}] run scoreboard players operation @s match_id = @s id # assign next ID for matching : O(E) scoreboard players operation 0-0-0-0-1 match_id > @e match_id # loop : O(SE) execute if score 0-0-0-0-1 match_id matches 1.. run function space:match_loop ``` ``` # space:match_loop.mcfunction # match targets : O(E) execute as @e[scores={match_id=1..}] if score @s id = 0-0-0-0-1 id run function space:matched # update next ID for matching : O(E) scoreboard players operation 0-0-0-0-1 match_id > @e match_id # loop yet if match-id not run out : O(1) execute if score 0-0-0-0-1 match_id matches 1.. run function space:match_loop ``` ``` # space:matched.mcfunction scoreboard players reset @s match_id tag @s add matched ``` 可以從內層向外解析: | function | Big-O | T | | -------- | -------- | --- | | matched | $O(1)$ | | | match_loop | $O(E)$ | $T(E+E+1)$ | | match | $O(SE)$ | $T(1+E+E+ST_2)≈T(2E+2SE)$ | S代表不同ID的數量,並且可以保證,因為ID數量不會比現存的實體總數還多, 即使ID比較多也只會根據現存的實體來篩選下一個要進行檢查的ID, 故時間複雜度為$O(E^2)$ --- ## 總結 實體選擇器是一個非常糟糕的操作手段: 1. 排序(sort)和數量限制(limit)是在記錄完滿足條件的實體後才排序與取出 (在效能上,這樣跟沒取數量是一樣的) 2. 取得名稱(user name)的方式是採用遍歷窮舉比對,以名稱操作只是在增加 $P$ 的數量 3. 在非必要情況,應當減少sort的選擇器條件, 因為這會使原本的複雜度從 $O(|E|)$ 變成 $O(|E|\log |E|)$ 4. 增加更多的選擇器條件並不會因此使效能變得更好,仍舊會遍歷每個實體並逐一比對條件 5. 在一般情況,我自己更推薦使用@s和UUID來取代任何的實體選擇器,