iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0

今日目標,倒數計時。

Timer

既然要讀秒,那我們先嘗試使用 Timer,不過這個方法是不可行的,待會會說明,然後這邊為了方便,我們寫測試程式來 Demo。

示範

  1. 先給範例 code:
    package com.example.game;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class TimerDemo {
        private int time = 0;
        public void run() {
            // 建立 Timer
            Timer timer = new Timer();
    
            // 要執行的工作
            TimerTask task = new TimerTask () {
                public void run() {
                    doSomething(time++);
                }
            };
    
            // 開始執行,第一次執行延遲 0 秒,每次執行間隔 1 秒(1000毫秒)
            timer.schedule (task, 0, 1000);
        }
    
        public void doSomething(int n) {
            System.out.println(n);
        }
    }
    
  2. 我們對 run() 點右鍵,選擇 generate
  3. 然後選擇 Test...
  4. 再點 ok 就會自動產生一個 java class,叫做 TimerDemoTest,然後輸入以下內容:
    package com.example.game;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class TimerDemoTest {
        @Test
        public void testRun() throws InterruptedException {
            TimerDemo timer = new TimerDemo();
            timer.run();
    
            // 設定要等多久,不然他會直接停掉
            Thread.sleep(1000 * 5);
        }
    }
    
  5. 點旁邊的綠色箭頭,再選擇 Run 'testRun()'
  6. 這時候就會看到輸出慢慢地跑出 0~4

問題

我們剛才這樣做乍看好像沒什麼問題,把 time 改成我們要的秒數,比如 20,讓他往回扣就好,然後減到 0 就停止這個 timer,但最大的問題在於,當玩家打出手牌時,應該要直接停止這個 timer 並且輪到下一次的倒數,雖然我們可以做到停止,但沒辦法知道它哪時候停止。
因此,我們要改用別的方式來實現。

Scheduled Executor

我們一樣示範一下程式用法。

示範

  1. 先修改剛才 TimerDemo 的程式碼,內容為:
    package com.example.game;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class TimerDemo {
        private int time = 0;
        public void run() {
            ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
            timer.scheduleAtFixedRate(()-> {
                doSomething(time++);
            }, 0, 1, TimeUnit.SECONDS);
        }
        public void doSomething(int n) {
            System.out.println(n);
        }
    }
    
  2. 再去剛剛的 TimerDemoTest 執行一次,會發現跟剛才一樣的輸出結果
  3. 我們稍微修改一下原本的 TimerDemo,讓 run 多一個參數,用來看現在是哪個 timer:
    package com.example.game;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class TimerDemo {
        private int time = 0;
        public void run(int task) {
            ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
    
            // 要執行什麼
            // 也定義延遲 0 秒開始執行,每次間隔 1 秒,最方便的是,可以透過 TimeUnit.SECONDS 來自定義單位,不必自己換算
            timer.scheduleAtFixedRate(()-> {
                doSomething(task, time++);
            }, 0, 1, TimeUnit.SECONDS);
        }
        public void doSomething(int task, int n) {
            System.out.println(task + ": " + n);
        }
    }
    
  4. 在 TimerDemoTest 再增加一個 timer,修改一下 TimerDemoTest,多用一個 timer,修改後完整內容:
    package com.example.game;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class TimerDemoTest {
        @Test
        public void testRun() throws InterruptedException {
            TimerDemo timer1 = new TimerDemo();
            timer1.run(1);
    
            TimerDemo timer2 = new TimerDemo();
            timer2.run(2);
    
            Thread.sleep(1000 * 5);
        }
    }
    
  5. 在 TimerDemoTest 再執行一次,就會發現兩個 timer 同時運作,並且可以根據前面的 task 知道是哪個 timer,但到這邊為止,用剛才的 Timer 都做得到,但我們再修改一下 TimerDemo,完整內容為:
    package com.example.game;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class TimerDemo {
        private int time = 0;
        public void run(int task) throws InterruptedException {
            ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
            timer.scheduleAtFixedRate(()-> {
                doSomething(task, time++);
    
                // 設定一下中斷點,執行 5 秒就結束
                if (time > 5) {
                    timer.shutdown();
                }
            }, 0, 1, TimeUnit.SECONDS);
    
            // 等 timer 執行 7 秒
            timer.awaitTermination(7, TimeUnit.SECONDS);
        }
        public void doSomething(int task, int n) {
            System.out.println(task + ": " + n);
        }
    }
    
  6. 這時候會發現,timer2 在等了 timer1 執行完才開始執行,也就是說,我們可以透過 awaitTermination() 等待前一個結束後,讓下一個 timer 執行

實作

既然搞懂 Timer 怎麼弄,那我們就來定義自己的 Timer 吧,並生出對應方法,之後也比較好調用。

  1. 在 game package 底下建立一個 java class,名稱為 GameTimer,內容為:
    package com.example.game;
    
    import com.example.call.Callable;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class GameTimer {
        private ScheduledExecutorService timer;
        private Integer time = 1;
    
        // 初始化
        public void init(int time) {
            this.timer = Executors.newSingleThreadScheduledExecutor();
            this.time = time;
        }
    
    
        // 倒數 (參數 printer 我們還沒定義)
        public void countDown(Callable printer) {
            try {
                Thread.sleep(1000);
            }
            catch (Exception e) {
                System.out.println(e.getMessage());
            }
            this.timer.scheduleAtFixedRate(()-> {
                printer.call(time.toString());
                time--;
                if (time <= 0) {
                    stop();
                }
            }, 1, 1, TimeUnit.SECONDS);
        }
    
        // 等待的最大秒數
        public void await(int timeoutWithSeconds) {
            try{
                this.timer.awaitTermination(timeoutWithSeconds, TimeUnit.SECONDS);
            }
            catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    
        // 停止 Timer
        public void stop() {
            this.timer.shutdown();
        }
    }
    
  2. 我們再來定義當執行了這個 Timer,我們希望可以用呼叫的時候才決定怎麼把倒數的數字傳過去,相當在參數放一個函數
  3. 建立一個 package,名稱為 call
  4. 在 call package 底下建立一個 java interface 名稱為 Callable,內容為:
    package com.example.call;
    
    public interface Callable {
        public void call(String param);
    }
    
  5. 呼叫的方式很簡單,我們對 GameTimer 產生一個測試檔案來示範吧,內容為:
    package com.example.game;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    class GameTimerTest {
        @Test
        public void testGameTimer() throws InterruptedException {
            GameTimer timer = new GameTimer();
            timer.init(5);
            timer.countDown(System.out::println);
            timer.await(3);
            timer.stop();
        }
    }
    

今天先講解如何實現倒數計時,之後會將這個套用到遊戲進行的時候~~/images/emoticon/emoticon07.gif


上一篇
Day 26 - 手牌
下一篇
Day 28 - 出牌
系列文
Spring Boot... 深不可測31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言