如何问玩家“吾与徐公孰美?” - 编程开发 - Minecraft(我的世界)中文论坛 -.htm

如何问玩家“吾与徐公孰美?” - 编程开发 - Minecraft(我的世界)中文论坛 -

Minecraft(我的世界)中文论坛

 找回密码
 注册(register)

!header_login!

只需一步,立刻登录

查看: 4008|回复: 12
打印 上一主题 下一主题

[插件开发教程] 如何问玩家“吾与徐公孰美?”

[复制链接]
海螺螺 当前离线
积分
21867
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2015-2-24
查看详细资料

来自:四川

跳转到指定楼层
楼主
 楼主| 发表于 2020-2-13 02:05:56 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

您尚未登录,立即登录享受更好的浏览体验!

您需要 登录 才可以下载或查看,没有帐号?注册(register)

x
如何问玩家“吾与徐公孰美?”

本文发布于 https://izzel.io/2020/02/12/chat-with-future/

使用 CompletableFuture 实现一个简单的对话。


关于对话


对话并不罕见,在 QuickShop 中,插件会向玩家询问“你要买几个东西”,玩家则在聊天栏中输入对应的值;在 PlotSquared 中,玩家需要不断地输入对应的命令来配置地皮生成的参数,而输入命令也是另一种形式的对话。


对话的实现,Bukkit 为开发者准备了一套 Conversations API,编程开发板块的这篇帖子有简单的介绍。


本篇将会介绍另一种实现它的方法,简单的可以概括为,在一个方法里流畅的处理对话,比如这样:


import org.bukkit.entity.Player;

public class SomeClass {

    public void ask(Player player) {
        player.sendMessage("吾与徐公孰美?");
        String answer = getAnswer(player);
        assert answer.equals("徐公不若君之美也!");
    }
}

接下来的篇幅,就将讨论上文的 getAnswer 如何实现。




实现对话,无非是这种逻辑:



  1. 发送给玩家问题

  2. 监听玩家的回复

  3. 处理玩家的回复

  4. 如果还有问题,跳到 1


按照正常的方法编写,我们需要一个监听器监听 PlayerChatEvent 或者它的异步版本,需要记录玩家的状态(玩家是不是在一个对话中 / 对话进行到了哪里),如果是诸葛亮王朗量级的超长对话,那么判断状态 / 更新状态的代码量将不堪设想。你还需要考虑玩家掉线/玩家不回答的情况,这就会又引入别的监听器和定时器。


因此,我们会想用上文代码一样的方式处理,无需记录状态,但是显然,getAnswer 不可能在调用的时候就有结果,玩家这时可能还不知道他被提了一个美不美的问题,而答案有可能会在未来提供,因此我们有了 Future 接口。


关于 Future


这是 Future 接口的定义:


package java.util.concurrent;
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

注意到上面加粗的有可能了吗?为什么会 有可能 呢?



  • 你可能不想问了(cancel

  • 玩家可能并不想理睬你(get 方法超时了)

  • 玩家掉线了(get 方法抛出了异常)

  • 齐威王突然召见你进宫(get 方法被中断了)

  • 玩家回答:徐公不若君之美也!


可以得知,你能准确的从玩家嘴里问出有效的答案实属不易,而 Future 接口就可以表示一个可能出现异常的、会在未来得到结果的东西。Future 的泛型 V,则表示得到的值。


因此,上面的代码可以改成这样:


public class SomeClass {

    public Future<String> getAnswer(Player player) {
        return // ???
    }

    public void ask(Player player) {
        try {
            player.sendMessage("吾与徐公孰美?");
            String answer = getAnswer(player).get();
            assert answer.equals("徐公不若君之美也!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

但是,Future 从哪里来呢?本篇的答案是 CompletableFuture


关于 CompletableFuture


望文生义,CompletableFuture 代表着可以完成Future,这与本篇的目的不谋而合(不然呢):玩家输入消息后,getAnswer 方法返回的 Future 就该完成了。


我们来了解一下 CompletableFuture 中比较重要的几个方法:


package java.util.concurrent;
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    public CompletableFuture() { }
    public T join();
    public boolean complete(T value);
    public boolean completeExceptionally(Throwable ex);
}


  • CompletableFuture 实现了 Future 接口,自然有 Future 接口的所有方法

  • 无参构造方法得到一个崭新出厂的 Future

  • join 方法与 get 的效果类似,但有些许不同,在使用者看来最显著的区别就是,join 并不让你强制处理异常,虽然异常永远都在

  • completecompleteExceptionally 分别代表正常完成和异常完成


因此,我们不难将上面的代码改成这样:


public class SomeClass {

    public Future<String> getAnswer(Player player) {
        CompletableFuture<String> future = new CompletableFuture();
        // 在别的地方调用 future.complete()
        return future;
    }

    public void ask(Player player) {
        try {
            player.sendMessage("吾与徐公孰美?");
            String answer = getAnswer(player).get();
            assert answer.equals("徐公不若君之美也!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

于是,我们也就不难写出以下的代码:


public class SomeClass {

    public void ask(Player player); // ...

    public Future<String> getAnswer(Player player) {
        CompletableFuture<String> future = new CompletableFuture<>();
        AskLifeExperience listener = new AskLifeExperience(player.getUniqueId(), future);
        Bukkit.getPluginManager().registerEvents(listener, plugin);
        return future;
    }

    public class AskLifeExperience implements Listener {

        private final UUID uuid;
        private final CompletableFuture<String> future;

        public AskLifeExperience(UUID uuid, CompletableFuture<String> future) {
            this.uuid = uuid;
            this.future = future;
        }

        @EventHandler
        public void on(AsyncPlayerChatEvent event) {
            if (event.getPlayer().getUniqueId().equals(uuid)) {
                future.complete(event.getMessage());
                HandlerList.unregisterAll(this);
            }
        }
    }
}

逻辑清晰明了,注册一个监听器,在玩家聊天的时候完成 Future


转眼一想,既然 getAnswer 需要一定时间才会取得答案,那 ask 方法不就会消耗很多时间了吗?因此,我们要异步调用 ask


关于 Minecraft 服务器的同步与异步


当不在主线程进行操作的时候,我们都应该想一想,这样安全吗?


从上到下看一遍,不难问出这些问题:



  • sendMessage 安全吗?

  • 异步注册事件是安全的吗?

  • CompletableFuture#complete 安全吗?(不然呢)

  • Future#get 方法一定会返回吗?


根据一篇写的很不错的文档(这篇文档对水桶的 scheduler 有较为详细的介绍),这几个东西是线程安全的:



  • sendMessage (发包)

  • Bukkit 的 scheduler

  • PluginManager#callEvent(event)


因此应该将注册事件部分的代码通过 Scheduler 转移到主线程完成。


最终的完整(但不完善)的方法如下,监听器与上文相同:


public class SomeClass {

    private Plugin plugin = null;

    public void ask(Player player) {
        Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
            try {
                player.sendMessage("吾与徐公孰美?");
                String answer = getAnswer(player).get(15, TimeUnit.SECONDS);
                assert answer.equals("徐公不若君之美也!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    public Future<String> getAnswer(Player player) {
        CompletableFuture<String> future = new CompletableFuture<>();
        Bukkit.getScheduler().runTask(plugin, () -> {
            AskLifeExperience listener = new AskLifeExperience(player.getUniqueId(), future);
            Bukkit.getPluginManager().registerEvents(listener, plugin);
        });
        return future;
    }
}

当然,代码写完,还应该问自己几个问题:



  • 我们在监听器里在事件触发的时候取消注册,万一事件永远不触发呢?

  • 玩家离线后,Player 实例不再可用,怎么办呢?


这些问题不是本篇重点,就不说了。


总结


可以看出,优美的写一串对话,所需代码量其实并不多,寥寥数十行就可以了。


线程安全十分重要。


CompletableFuture 还有许多实用的方法,可以用于各种耗时的操作,如 获取数据库的信息后,将其应用于服务器中。希望读者能够自行多加了解。


zzzz 编写了一篇协程教程,可以写出与本篇主方法非常类似的代码,虽然背后的原理大不相同,比如它全部在主线程上运行。


tdiant 编写了一篇十分全面的水桶教程,对 Scheduler 和其他部分都有很多讲解。



来自群组: PluginsCDTribe

评分

参与人数 21人气 +39金粒 +268宝石 +24贡献 +1收起 理由
daoiojoig+ 2Ssssssssssssssssssss
白色的小熊+ 1MCBBS有你更精彩~
Legoshi+ 2MCBBS有你更精彩~
乙烯_中国+ 24MCBBS有你更精彩~
SKFT+ 2人间迷惑行为
晓夜Port+ 1Ssssssssssssssssssss
桃渊林+ 2论坛需要更多类似的新颖的教程.
gooding300+ 3+ 33神乎其技!6的飞起!
天宫时雨+ 1+ 15MCBBS有你更精彩~
速食冻橘+ 1+ 20完了一到Future我就看不懂了
一梦入神宫.+ 2+ 20强!
土球球+ 4+ 50+ 1优秀
Lss233+ 2神乎其技!6的飞起!
秋韵+ 1MCBBS有你更精彩~
轻光233+ 2+ 20MCBBS有你更精彩!!
Zapic+ 2MCBBS有你更精彩~
清晨w+ 2+ 30神乎其技!6的飞起!
ItIsEnderman+ 3+ 20MCBBS有你更精彩~
Sssss...+ 2Map+PlayerChatEvent不就好了
3TUSK+ 50这个文风值得学习

查看全部评分

帖子永久链接: 

Minecraft中文论坛 - 论坛版权1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
2、本站所有主题由该帖子作者发表,该帖子作者享有帖子相关版权
3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者的同意
4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
5、本帖若有内容转载自其它媒体,不代表本站赞同其观点和对其真实性负责
6、若本帖涉及任何版权问题,请立即告知本站,本站将及时予以删除并致以最深的歉意
7、Minecraft(我的世界)中文论坛管理员和版主有权不事先通知发贴者而删除本文

瑾某人 当前离线
积分
1302
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-2-12
查看详细资料

来自:河南

沙发
发表于 2020-2-13 15:36:33 | 只看该作者
这么优秀的教程为什么没人回复呢? (雾)   里面涉及了一些知识我没用过2333,感谢教程
回复

使用道具 举报

hhhh1014 当前离线
积分
9
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-2-14
查看详细资料

来自:山东

受到警告 板凳
发表于 2020-2-14 15:37:48 来自手机 | 只看该作者
nbnb(我没看懂)狗头
回复

使用道具 举报

mohui666 当前离线
积分
14965
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-2-13
查看详细资料

来自:山东

受到警告 地板
发表于 2020-2-14 16:14:43 来自手机 | 只看该作者
没看懂
回复

使用道具 举报

白叶Official 当前离线
积分
2844
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2018-4-7
查看详细资料

来自:广东

5#
发表于 2020-2-14 19:28:00 来自手机 | 只看该作者
徐公不是mi圈的佬嘛()
回复

使用道具 举报

FireworkPolymer 当前离线
积分
9154
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2015-11-29
查看详细资料

来自:湖南

6#
发表于 2020-2-14 22:03:35 | 只看该作者
对对对,这编的多好啊,真是大佬(其实我一个都没看懂)
回复

使用道具 举报

Co2_man 当前离线
积分
144
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-2-14
查看详细资料

来自:广东

受到警告 7#
发表于 2020-2-15 10:50:08 | 只看该作者
秀啊:|
回复

使用道具 举报

Karlatemp 当前离线
积分
3398
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2015-6-17
查看详细资料

来自:广东

8#
发表于 2020-2-15 14:45:18 来自手机 | 只看该作者
既然用了CompletableFuture为啥不用then...方法
回复

使用道具 举报

2782429220 当前离线
积分
107
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-2-13
查看详细资料

来自:河北

受到警告 9#
发表于 2020-2-16 12:07:37 来自手机 | 只看该作者
额awa看不懂
回复

使用道具 举报

lsj59956100 当前离线
积分
163
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2013-11-30
查看详细资料

来自:山西

受到警告 10#
发表于 2020-2-16 17:18:32 | 只看该作者
6666666666666666666666666666
回复

使用道具 举报

九仞 当前离线
积分
3565
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2015-8-15
查看详细资料

来自:浙江

11#
发表于 2020-2-16 19:57:57 | 只看该作者
我来顶一下,不错的帖子,简单明了,以例子来方便理解
回复

使用道具 举报

唯独i 当前离线
积分
424
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2015-9-12
查看详细资料

来自:福建

12#
发表于 2020-2-17 11:28:06 | 只看该作者
虽然没看懂 但还是感觉好厉害哈哈哈!
回复

使用道具 举报

2782429220 当前离线
积分
107
帖子
主题
精华
贡献
爱心
钻石
人气
下界之星
最后登录
1970-1-1
注册时间
2020-2-13
查看详细资料

来自:河北

受到警告 13#
发表于 2020-2-20 10:25:32 来自手机 | 只看该作者
没看懂
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册(register)

本版积分规则

Archiver|小黑屋|Mcbbs.net ( 京ICP备15023768号-1 ) | 京公网安备 11010502037624号 | 手机版

GMT+8, 2023-5-28 21:27 , Processed in 0.050215 second(s), Total 27, Slave 26 queries, Release: Build.2023.03.15 0137, Gzip On, Redis On.

"Minecraft"以及"我的世界"为美国微软公司的商标 本站与微软公司没有从属关系

© 2010-2022 我的世界中文论坛 版权所有 本站内原创内容版权属于其原创作者,除作者或版规特别声明外未经许可不得转载