[上一章](第二章-事件相关方法.md) [下一章](第四章-Server类和PluginManager类.md)
# 第二部分 第三章 计时器的介绍
参与编写者：`zzz1999`
#### 建议学习时间：3分钟
##### 学习要点：了解计时器的功能以及如何使用

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

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

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

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


二.代码

我们先来看一下nukkit的[Task](https://github.com/NukkitX/Nukkit/blob/master/src/main/java/cn/nukkit/scheduler/Task.java)类.

```java
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()`获取到计时器的实例类.
```java
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类.
```java
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()`方法,可以通过构造方法传入一份主类或任意你需要类实例的引用,方便编写代码)

```java

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类

```java
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分钟了,快下线休息一下眼睛吧!");
    }
}

```

> 以及一个主类

```java

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,加入时间作为判断依据
         > 
         ```java
         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分钟一到就会无尽发送消息
         > 
         ```java
         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();
             }
         }
         
         ```
> 最终从各种角度来看,小明更应该使用`第二种`方法



[上一章](第二章-事件相关方法.md) [下一章](第四章-Server类和PluginManager类.md)
