第三节 计时器的介绍

上一节 下一节

第二章 第三节 计时器的介绍

参与编写者:zzz1999

建议学习时间:3分钟

学习要点:了解计时器的功能以及如何使用

一.概念
1.计时器的作用是 立即,延时,循环或延时循环 某些任务,通过计时器我们可以实现例如 在线奖励 , 倒计时 等插件功能, 是一些复杂插件中经常用到的功能之一.

2.执行计时器代码块的单元叫做任务,我们可以编写任务代码来使用计时器的功能.

3.任务执行分为两种类型,一种是同步,一种是异步.同步通过主线程所调度管理执行,异步通过主线程调度管理,异步线程池执行.所以,太多的任务会导致服务器卡顿.

4.计时器的功能以minecraft刻(tick)作为单位,一刻为现实时间的0.05秒,现实时间的1秒为20刻,服务器每秒刻数由TPS决定,最高为20.

二.代码

我们先来看一下nukkit的Task类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package cn.nukkit.scheduler;

import cn.nukkit.Server;

/**
* 表达一个任务的类。<br>A class that describes a task.
*
* <p>一个任务可以被Nukkit服务器立即,延时,循环或延时循环执行。参见:{@link ServerScheduler}<br>
* A task can be executed by Nukkit server with a/an express, delay, repeat or delay&amp;repeat.
* See:{@link ServerScheduler}</p>
*
* <p>对于插件开发者,为确保自己任务能够在安全的情况下执行(比如:在插件被禁用时不执行),
* 建议让任务继承{@link PluginTask}类而不是这个类。<br>
* For plugin developers: To make sure your task will only be executed in the case of safety
* (such as: prevent this task from running if its owner plugin is disabled),
* it's suggested to use {@link PluginTask} instead of extend this class.</p>
*
* @author MagicDroidX(code) @ Nukkit Project
* @author 粉鞋大妈(javadoc) @ Nukkit Project
* @since Nukkit 1.0 | Nukkit API 1.0.0
*/
public abstract class Task implements Runnable {
private TaskHandler taskHandler = null;

public final TaskHandler getHandler() {
return this.taskHandler;
}

public final int getTaskId() {
return this.taskHandler != null ? this.taskHandler.getTaskId() : -1;
}

public final void setHandler(TaskHandler taskHandler) {
if (this.taskHandler == null || taskHandler == null) {
this.taskHandler = taskHandler;
}
}

/**
* 这个任务被执行时,会调用的过程。<br>
* What will be called when the task is executed.
*
* @param currentTick 服务器从开始运行到现在所经过的tick数,20ticks = 1秒,1tick = 0.05秒。<br>
* The elapsed tick count from the server is started. 20ticks = 1second, 1tick = 0.05second.
* @since Nukkit 1.0 | Nukkit API 1.0.0
*/
public abstract void onRun(int currentTick);

@Override
public final void run() {
this.onRun(taskHandler.getLastRunTick());
}

public void onCancel() {

}

public void cancel() {
try {
this.getHandler().cancel();
} catch (RuntimeException ex) {
Server.getInstance().getLogger().critical("Exception while invoking onCancel", ex);
}
}

}

我们可以捕捉到几个关键信息,首先我们可以看到onRun(int)方法,它是一个抽象方法并且nk开发者很贴心的写上了注释示意我们这是Task中会被运行到代码块.

还有onCancel()cancel()方法,onCancel()内部是一个空代码块,当Task被取消的时候(被任务调度器取消或者手动调用cancel()取消都会触发),这段代码块会被调用

比较常见的用途比如小游戏时间到的时候,一些判定胜负,给予奖励,关闭房间的一些代码可以用Task来实现或者调用.

我们还可以使用cancel()方法来强制取消一个Task,例如你想要写一个登录插件,每个玩家进入服务器时创建一个Task循环发送提示登录的信息,如果玩家输入了正确的密码并且登录成功才会取消掉这个Task.这时候就可以使用cancel方法了(仅举例,实际生产环境中,每个玩家进入创一个计时器是很费资源的,不会使用这种方法)

Handler是服务器调度Task所用到的一些东西,可以不懂,不妨碍计时器的使用.

开发者可以使用PluginBase#getServer()方法获取到Server对象,然后使用Server#getSchedule()获取到计时器的实例类.

1
2
3
4
5
6
public class Main extends PluginBase{
@Override
public void onEnable(){
ServerScheduler scheduler = this.getServer().getScheduler();
}
}

获取到后,可以创建 立即,延时,循环或延时循环 类型的计时器
立即(ServerSchedule#scheduleTask)
延时(ServerSchedule#scheduleDelayedTask)
循环(ServerSchedule#scheduleRepeatingTask)
延时循环(ServerSchedule#scheduleDelayedRepeatingTask)

计时器是需要一个被触发的时间间隔的,以tick为单位,如果是循环计时器,则是每次循环的时间,如果是延时,则是延迟多少tick才被触发,如果是延迟循环,则会要求填写延迟间隔与循环间隔.

举例将会在下一小节讲到

3.使用

下面会使用到PluginTask类,你需要对PluginTask类的方法做一个初步的了解,以及知道它继承了Task类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package cn.nukkit.scheduler;


import cn.nukkit.plugin.Plugin;

/**
* 插件创建的任务。<br>Task that created by a plugin.
*
* <p>对于插件作者,通过继承这个类创建的任务,可以在插件被禁用时不被执行。<br>
* For plugin developers: Tasks that extend this class, won't be executed when the plugin is disabled.</p>
*
* <p>另外,继承这个类的任务可以通过{@link #getOwner()}来获得这个任务所属的插件。<br>
* Otherwise, tasks that extend this class can use {@link #getOwner()} to get its owner.</p>
*
* 下面是一个插件创建任务的例子:<br>An example for plugin create a task:
* <pre>
* public class ExampleTask extends PluginTask&lt;ExamplePlugin&gt;{
* public ExampleTask(ExamplePlugin plugin){
* super(plugin);
* }
*
* {@code @Override}
* public void onRun(int currentTick){
* getOwner().getLogger().info("Task is executed in tick "+currentTick);
* }
* }
* </pre>
*
* <p>如果要让Nukkit能够延时或循环执行这个任务,请使用{@link ServerScheduler}。<br>
* If you want Nukkit to execute this task with delay or repeat, use {@link ServerScheduler}.</p>
*
* @param <T> 这个任务所属的插件。<br>The plugin that owns this task.
* @author MagicDroidX(code) @ Nukkit Project
* @author 粉鞋大妈(javadoc) @ Nukkit Project
* @since Nukkit 1.0 | Nukkit API 1.0.0
*/
public abstract class PluginTask<T extends Plugin> extends Task {

protected final T owner;

/**
* 构造一个插件拥有的任务的方法。<br>Constructs a plugin-owned task.
*
* @param owner 这个任务的所有者插件。<br>The plugin object that owns this task.
* @since Nukkit 1.0 | Nukkit API 1.0.0
*/
public PluginTask(T owner) {
this.owner = owner;
}

/**
* 返回这个任务的所有者插件。<br>
* Returns the owner of this task.
*
* @return 这个任务的所有者插件。<br>The plugin that owns this task.
* @since Nukkit 1.0 | Nukkit API 1.0.0
*/
public final T getOwner() {
return this.owner;
}

}

我们可以通过创建一个继承PluginTask的类来创建一个PluginTask(为什么不直接继承Task类?,因为这个类添加了getOwner()方法,可以通过构造方法传入一份主类或任意你需要类实例的引用,方便编写代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

package net.noyark.NukkitLearn;

import cn.nukkit.scheduler.PluginTask;

public class NukkitLearnExampleTask extends PluginTask<Main> {
public NukkitLearnExampleTask(Main owner) {
super(owner);
}

@Override
public void onRun(int i) {

}
}


上面代码是一个简单Task类的样例,我们现在带入一个插件情境进一步说明计时器的用法.

小明是一个服主,他非常喜欢玩家来他的服务器玩并且希望玩家每玩45分钟能够停下来休息一下保护视力.于是他想要开发一个插件,当玩家单次进入服务器并且呆了45分钟后,能够发送一条信息提示(Player#sendMessage(String)玩家休息一会)

于是我们来头脑风暴一下,我们知道计时器有4种类型,不知道你还能不能想得起来

立即(ServerSchedule#scheduleTask)
延时(ServerSchedule#scheduleDelayedTask)
循环(ServerSchedule#scheduleRepeatingTask)
延时循环(ServerSchedule#scheduleDelayedRepeatingTask)

小明火速写好了一个Task类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package net.noyark.NukkitLearn;

import cn.nukkit.Player;
import cn.nukkit.scheduler.PluginTask;

public class PresentationRestTask extends PluginTask<Main> {

private Player player;

public PresentationRestTask(Main owner, Player player) {
super(owner);
this.player = player;
}

@Override
public void onRun(int i) {
player.sendMessage("你已经玩了45分钟了,快下线休息一下眼睛吧!");
}
}

以及一个主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

package net.noyark.NukkitLearn;

import cn.nukkit.event.EventHandler;
import cn.nukkit.event.EventPriority;
import cn.nukkit.event.Listener;
import cn.nukkit.event.player.PlayerJoinEvent;
import cn.nukkit.plugin.PluginBase;

public class Main extends PluginBase implements Listener {

@EventHandler(priority = EventPriority.LOW,ignoreCancelled = true)
public void onJoin(PlayerJoinEvent event){

}

}

思考过程

  • 使用立刻类型的计时器
    • 使用立刻类型的计时器,在主类onJoin方法中填写入代码

      this.getServer().getScheduler().scheduleTask(new PresentationRestTask(this,event.getPlayer()));
      小明找了个玩家小红进入服务器,小红刚进入服务器,左上角就被发了消息:“你已经玩了45分钟了,快下线休息一下眼睛吧!”.

  • 使用延迟类型的计时器
    • 使用延时类型的计时器,在主类onJoin方法中填写入代码

      this.getServer().getScheduler().scheduleDelayedTask(new PresentationRestTask(this,event.getPlayer()),54000);
      小明找了个玩家小红进入服务器,小红刚进入服务器45分钟后,左上角才被发了消息:“你已经玩了45分钟了,快下线休息一下眼睛吧!”.

  • 使用循环类型的计时器
    • 似乎这很不符合我们的常理,并且增加了很多不必要的开销
    • 修改Task,加入时间作为判断依据
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      package net.noyark.NukkitLearn;

      import cn.nukkit.Player;
      import cn.nukkit.scheduler.PluginTask;

      public class PresentationRestTask extends PluginTask<Main> {

      private Player player;
      private long stp;

      public PresentationRestTask(Main owner, Player player,long stp) {
      super(owner);
      this.player = player;
      this.stp = stp;
      }

      @Override
      public void onRun(int i) {
      if (System.currentTimeMillis() >= stp) {
      player.sendMessage("你已经玩了45分钟了,快下线休息一下眼睛吧!");
      this.cancel();
      }
      }
      }

    • 使用循环类型的计时器,在主类onJoin方法中填写入代码

      this.getServer().getScheduler().scheduleRepeatingTask(new PresentationRestTask(this,event.getPlayer(),System.currentTimeMillis() + (45 * 60 * 1000)),60*20);
      可以达成效果,但是实在是太麻烦

    • 使用延时循环类型的计时器,在主类onJoin方法中填写入代码

      this.getServer().getScheduler().scheduleDelayedRepeatingTask(new PresentationRestTask(this,event.getPlayer()),45 * 60 * 20,0);
      同时计时器也需要修改,否则就会45分钟一到就会无尽发送消息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      package net.noyark.NukkitLearn;

      import cn.nukkit.Player;
      import cn.nukkit.scheduler.PluginTask;

      public class PresentationRestTask extends PluginTask<Main> {

      private Player player;

      public PresentationRestTask(Main owner, Player player) {
      super(owner);
      this.player = player;
      }

      @Override
      public void onRun(int i) {
      player.sendMessage("你已经玩了45分钟了,快下线休息一下眼睛吧!");
      this.cancel();
      }
      }

最终从各种角度来看,小明更应该使用第二种方法

上一节 下一节