本教程將以"手持發射器箭矢機槍"功能為例,帶你掌握Java語言基礎和Bukkit API的核心概念,最終實現自主開發插件。
我們將通過剖析一個實際Java代碼文件,逐步解析其運作機制,幫助你順利將現有編程知識遷移到Java和Bukkit開發中。
注:本教程基于RainyxinMAIN插件中的某個功能模塊編寫,教程中提到的RainyxinMAIN主類請根據實際情況進行相應調整。
目錄
1. 引言:從Python/C#到Java與Bukkit
2. 準備工作:搭建開發環境
2.1 Java Development Kit (JDK)
2.2 集成開發環境 (IDE)
2.3 構建工具 (Maven/Gradle)
2.4 Bukkit/Spigot/PaperMC服務器
3. 核心概念速覽:Java與Bukkit特有之處
3.1 Java語言特性與Python/C#的對比
3.2 Bukkit API核心:事件、監聽器、調度器
3.3 項目結構:pom.xml與plugin.yml
4. 代碼深度解析:箭矢機槍功能實現
4.1 類定義與構造函數
4.2?onPlayerInteract:玩家交互事件監聽
4.3 核心:Bukkit調度器與持續射擊任務
4.4 箭矢生成、消耗與擴散邏輯
4.5 停止射擊的條件與清理
4.6 輔助方法:查找箭矢
5. 構建、部署與測試
6. 擴展與進階
7. 總結
1. 引言:從Python/C#到Java與Bukkit
如果你熟悉Python的簡潔特性或C#的強類型系統,那么過渡到Java會相對容易。Java與C#在語法上有很多相似之處,因為它們都受到C++的深刻影響。作為一門強類型、面向對象的語言,Java通常需要編譯為字節碼(.class文件)才能在JVM上運行。
Bukkit API是Minecraft服務器插件開發的主流框架之一(目前更多使用其衍生項目如Spigot或PaperMC)。它提供了一系列接口和類,用于與Minecraft服務器交互:監聽游戲事件、修改游戲世界,以及實現自定義功能。
目標功能:
當玩家主手持石頭按鈕(Stone Button),副手持發射器(Dispenser)并右鍵時,持續發射箭矢——就像機槍一樣!箭矢的精準度會隨連續射擊逐漸降低,并有概率不消耗箭矢。
2. 準備工作:搭建開發環境
開始編寫代碼前,需要完成以下環境配置。
2.1 Java Development Kit (JDK)
必須安裝JDK(開發工具包),而非僅JRE(運行時環境)。推薦使用OpenJDK 21或更高版本,因為Minecraft 1.21.*通常需要新版Java支持。
- 下載:訪問Adoptium(Eclipse Temurin)或Oracle JDK官網
- 安裝:按照安裝向導完成
2.2 集成開發環境 (IDE)
優秀的IDE能顯著提升開發效率:
- 推薦:IntelliJ IDEA Community Edition(免費且功能全面)
- 備選:Eclipse或VS Code(需安裝Java插件)
2.3 構建工具 (Maven/Gradle)
Minecraft插件項目通常使用Maven或Gradle管理依賴和構建流程。它們能自動下載所需庫文件并編譯代碼。本文以Maven為例:
- Maven:多數IDE已內置,也可從Apache Maven官網單獨下載
2.4 Bukkit/Spigot/PaperMC服務器
需要本地Minecraft服務器用于插件測試:
- 下載:從PaperMC官網獲取最新版paperclip.jar
- 運行:新建文件夾存放jar文件,首次運行會生成eula.txt(需同意EULA)及服務器配置文件
3. 核心概念速覽:Java與Bukkit特有之處
3.1 Java語言特性與Python/C#的對比
- 強類型 (Strongly Typed):
- Python:?
x = 10
,?y = "hello"
?(動態類型)。 - C#:?
int x = 10; string y = "hello";
?(強類型)。 - Java:?
int x = 10; String y = "hello";
?(強類型)。變量聲明時必須指定類型。
- Python:?
- 語句結束符:
- Python:?換行。
- C#/Java:?
;
?(分號)。
- 代碼塊:
- Python:?縮進。
- C#/Java:?
{}
?(花括號)。
- 類與對象:
- Python:?
class MyClass:
,?obj = MyClass()
. - C#:?
class MyClass { }
,?MyClass obj = new MyClass();
. - Java:?
public class MyClass { }
,?MyClass obj = new MyClass();
?(與C#非常相似,但new
關鍵字是必須的)。
- Python:?
- 訪問修飾符:?
public
,?private
,?protected
,?default
?(包級私有)。- Python:?
_name
?(約定私有),?__name
?(名稱修飾)。 - C#/Java:?
public
?(公開),?private
?(私有),?protected
?(受保護的)。
- Python:?
- 接口 (Interface):
- C#:?
interface IMyInterface { void DoSomething(); }
. - Java:?
interface MyInterface { void doSomething(); }
?(與C#非常相似,類實現接口使用implements
關鍵字)。
- C#:?
- 泛型 (Generics):?
<T>
?在Java中廣泛使用,類似于C#中的泛型,用于在編譯時提供類型安全。Map<UUID, BukkitTask>
: 一個映射,鍵是UUID
類型,值是BukkitTask
類型。對應Python中的dict[uuid.UUID, Any]
或C#中的Dictionary<Guid, Task>
。
- Lambda表達式 (Lambda Expressions):
- Python:?
lambda x: x + 1
. - C#:?
x => x + 1
. - Java:?
(x) -> x + 1
?(在函數式接口上下文中使用,如Runnable
、Consumer
等)。
- Python:?
3.2 Bukkit API核心:事件、監聽器、調度器
- 事件 (Events):?Minecraft游戲中發生的任何事情,如玩家交互、方塊破壞、實體生成等,都會觸發一個事件。
PlayerInteractEvent
: 玩家與方塊或空氣交互時觸發。PlayerQuitEvent
: 玩家離開服務器時觸發。
- 監聽器 (Listeners):?你創建的類,用于“監聽”并響應特定的事件。
- 需要實現
org.bukkit.event.Listener
接口。 - 事件處理方法需要用
@EventHandler
注解標記。
- 需要實現
- 調度器 (Scheduler):?Bukkit提供了一個任務調度系統 (
BukkitScheduler
),用于在Minecraft主線程(重要的,所有與游戲對象交互都必須在主線程)或異步線程中執行任務。runTaskTimer(plugin, task, delay, period)
: 最常用的方法之一,用于重復執行任務。plugin
: 你的主插件實例。task
: 要執行的代碼(通常是Lambda表達式或Runnable
實例)。delay
: 首次執行前的延遲(單位:游戲刻,1秒=20刻)。period
: 任務重復執行的周期(單位:游戲刻)。- 注意:?Minecraft的邏輯和渲染都在一個主線程上,所以大多數Bukkit API調用必須在這個線程上進行。
runTaskTimer
默認就是在主線程上運行任務。
- 重要類:
Player
: 代表一個在線玩家。ItemStack
: 代表一個物品堆疊。Material
: 代表一種方塊或物品的類型(如Material.STONE_BUTTON
)。UUID
: 玩家的唯一標識符,即使玩家改名,UUID也不會變。常用于存儲與特定玩家相關的數據。Vector
: 3D向量,用于表示方向或速度。Arrow
: 箭矢實體。
3.3 項目結構:pom.xml
與plugin.yml
pom.xml
?(Maven項目對象模型):
這是Maven項目的配置文件,用于聲明項目信息、依賴項、構建插件等。
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>com.rainyxinmain</groupId> | |
<artifactId>rainyxinmain</artifactId> | |
<version>1.0-SNAPSHOT</version> | |
<packaging>jar</packaging> | |
<properties> | |
<maven.compiler.source>21</maven.compiler.source> <!-- 你的Java版本 --> | |
<maven.compiler.target>21</maven.compiler.target> <!-- 你的Java版本 --> | |
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |
</properties> | |
<repositories> | |
<!-- SpigotMC/PaperMC 庫,提供Bukkit API --> | |
<repository> | |
<id>papermc-repo</id> | |
<url>https://repo.papermc.io/repository/maven-public/</url> | |
</repository> | |
</repositories> | |
<dependencies> | |
<!-- Bukkit/PaperMC API 依賴 --> | |
<dependency> | |
<groupId>io.papermc.paper</groupId> | |
<artifactId>paper-api</artifactId> | |
<version>1.21-R0.1-SNAPSHOT</version> <!-- 根據你的服務器版本調整 --> | |
<scope>provided</scope> <!-- 插件在服務器運行時才需要此API,服務器已提供 --> | |
</dependency> | |
</dependencies> | |
<build> | |
<plugins> | |
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-compiler-plugin</artifactId> | |
<version>3.8.1</version> | |
<configuration> | |
<source>${maven.compiler.source}</source> | |
<target>${maven.compiler.target}</target> | |
</configuration> | |
</plugin> | |
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-shade-plugin</artifactId> | |
<version>3.2.4</version> | |
<executions> | |
<execution> | |
<phase>package</phase> | |
<goals> | |
<goal>shade</goal> | |
</goals> | |
<configuration> | |
<createDependencyReducedPom>false</createDependencyReducedPom> | |
</configuration> | |
</execution> | |
</executions> | |
</plugin> | |
</plugins> | |
</build> | |
</project> |
plugin.yml
:
這是一個放置在你的插件JAR文件根目錄下的YAML文件,用于告訴Minecraft服務器你的插件叫什么、作者是誰、主類在哪里等信息。
name: RainyXinMain | |
version: 1.0-SNAPSHOT | |
main: com.rainyxinmain.rainyxinmain.RainyxinMAIN # 你的主插件類路徑 | |
api-version: 1.21 # 你的服務器API版本 | |
authors: [RainyXin] | |
description: A custom plugin with various features. | |
permissions: # 插件需要的權限 | |
rainyxinmain.feature.continuousarrow: | |
description: Allows players to use the continuous arrow firing feature. | |
default: op # 默認只給OP(操作員) |
請確保你的主插件類繼承自org.bukkit.plugin.java.JavaPlugin
,并且在onEnable()
方法中注冊事件監聽器:
package com.rainyxinmain.rainyxinmain; | |
import com.rainyxinmain.rainyxinmain.features.ContinuousArrowFireListener; | |
import org.bukkit.plugin.java.JavaPlugin; | |
public final class RainyxinMAIN extends JavaPlugin { | |
@Override | |
public void onEnable() { | |
// 當插件啟動時,注冊事件監聽器 | |
getServer().getPluginManager().registerEvents(new ContinuousArrowFireListener(this), this); | |
getLogger().info("RainyXinMain has been enabled!"); | |
} | |
@Override | |
public void onDisable() { | |
// 當插件關閉時執行的清理工作 (可選) | |
getLogger().info("RainyXinMain has been disabled!"); | |
} | |
} | |
4. 代碼深度解析:箭矢機槍功能實現
package com.rainyxinmain.rainyxinmain.features; | |
import com.rainyxinmain.rainyxinmain.RainyxinMAIN; // 導入主插件類 | |
import org.bukkit.Bukkit; // 導入Bukkit主類,用于訪問調度器等 | |
import org.bukkit.Material; // 導入Material枚舉,表示物品類型 | |
import org.bukkit.entity.Arrow; // 導入Arrow實體類 | |
import org.bukkit.entity.Player; // 導入Player實體類 | |
import org.bukkit.event.EventHandler; // 導入EventHandler注解 | |
import org.bukkit.event.Listener; // 導入Listener接口 | |
import org.bukkit.event.block.Action; // 導入Action枚舉,表示交互動作 | |
import org.bukkit.event.player.PlayerInteractEvent; // 導入玩家交互事件 | |
import org.bukkit.event.player.PlayerItemHeldEvent; // 導入玩家手持物品改變事件 | |
import org.bukkit.event.player.PlayerQuitEvent; // 導入玩家退出事件 | |
import org.bukkit.event.player.PlayerSwapHandItemsEvent; // 導入玩家交換主副手物品事件 | |
import org.bukkit.inventory.ItemStack; // 導入ItemStack類,表示物品堆疊 | |
import org.bukkit.scheduler.BukkitTask; // 導入BukkitTask類,表示調度器任務 | |
import org.bukkit.util.Vector; // 導入Vector類,表示3D向量 | |
import java.util.HashMap; // 導入HashMap,用于存儲鍵值對 | |
import java.util.Map; // 導入Map接口 | |
import java.util.UUID; // 導入UUID類 | |
import org.bukkit.Sound; // 導入Sound枚舉,用于播放聲音 | |
import java.util.Random; // 導入Random類,用于生成隨機數 | |
public class ContinuousArrowFireListener implements Listener { | |
// 這行定義了一個公共類,名為ContinuousArrowFireListener,并聲明它實現了Listener接口。 | |
// 實現了Listener接口的類才能被Bukkit的事件系統識別為事件監聽器。 | |
// 類似于C#中實現某個接口:public class MyListener : IMyListener | |
private final RainyxinMAIN plugin; // 存儲主插件實例的引用,final表示其在初始化后不能被修改。 | |
private final Map<UUID, BukkitTask> activeFiringTasks; // 一個Map,用于存儲正在射擊的玩家(UUID)及其對應的BukkitTask。 | |
// 類似于Python的字典 {UUID: Task} 或 C#的 Dictionary<Guid, Task>。 | |
private final Map<UUID, Long> firingStartTime; // 存儲玩家開始持續射擊的時間戳,用于計算箭矢擴散。 | |
private final Random random; // 用于生成隨機數,例如箭矢消耗的概率。 | |
private final Map<UUID, ItemStack> cachedArrowStacks; // 緩存玩家當前使用的箭矢堆疊,避免重復查找。 | |
public ContinuousArrowFireListener(RainyxinMAIN plugin) { | |
// 構造函數,在創建這個類的實例時被調用。 | |
// 它接收一個RainyxinMAIN類型的參數,即你的主插件實例。 | |
this.plugin = plugin; // 將傳入的插件實例賦值給類的成員變量。 | |
this.activeFiringTasks = new HashMap<>(); // 初始化HashMap,空字典/哈希表。 | |
this.firingStartTime = new HashMap<>(); // 初始化HashMap。 | |
this.random = new Random(); // 初始化隨機數生成器。 | |
this.cachedArrowStacks = new HashMap<>(); // 初始化HashMap。 | |
} | |
@EventHandler // @EventHandler注解表示這個方法是一個事件處理器,它將監聽PlayerInteractEvent事件。 | |
// 類似于Python的裝飾器 @event_handler 或 C#的特性 [EventHandler]。 | |
public void onPlayerInteract(PlayerInteractEvent event) { | |
Player player = event.getPlayer(); // 獲取觸發事件的玩家實例。 | |
Action action = event.getAction(); // 獲取玩家的交互動作(右鍵、左鍵等)。 | |
// 只在右鍵交互時觸發 (右鍵空氣或右鍵方塊) | |
if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) { | |
return; // 如果不是右鍵,則直接返回,不執行后續代碼。 | |
} | |
// 檢查玩家是否擁有特定權限 | |
if (!player.hasPermission("rainyxinmain.feature.continuousarrow")) { | |
return; // 如果玩家沒有權限,則返回。 | |
} | |
// 檢查玩家是否手持正確的物品 | |
ItemStack mainHand = player.getInventory().getItemInMainHand(); // 獲取主手物品堆疊。 | |
ItemStack offHand = player.getInventory().getItemInOffHand(); // 獲取副手物品堆疊。 | |
// 檢查主手是否是石頭按鈕,副手是否是發射器 | |
boolean hasRequiredItems = mainHand.getType() == Material.STONE_BUTTON && offHand.getType() == Material.DISPENSER; | |
if (hasRequiredItems) { | |
// 如果該玩家已經有射擊任務在運行,則不做任何事情,避免重復啟動。 | |
if (activeFiringTasks.containsKey(player.getUniqueId())) { | |
return; | |
} | |
// 緩存玩家當前背包中的箭矢堆疊,避免每次射擊都重新查找。 | |
// 稍后會解釋findArrowInInventory方法。 | |
cachedArrowStacks.put(player.getUniqueId(), findArrowInInventory(player)); | |
// 為該玩家啟動一個新的持續射擊任務。 | |
firingStartTime.put(player.getUniqueId(), System.currentTimeMillis()); // 記錄開始時間(毫秒)。 | |
// Bukkit調度器:runTaskTimer 方法用于在指定延遲后,以指定周期重復執行一個任務。 | |
// plugin: 插件實例,指示任務屬于哪個插件。 | |
// () -> { ... }: 這是一個Java Lambda表達式,代表一個匿名函數/可運行的任務。 | |
// 0L: 首次執行的延遲(0刻,即立即執行)。L表示是long類型。 | |
// 1L: 任務重復的周期(1刻,即每游戲刻執行一次,Minecraft每秒20刻)。 | |
BukkitTask task = Bukkit.getScheduler().runTaskTimer(plugin, () -> { | |
// 這個Lambda表達式中的代碼會在每游戲刻被執行。 | |
// 檢查玩家是否仍然在線。如果下線了,停止任務。 | |
if (!player.isOnline()) { | |
stopFiringTask(player.getUniqueId()); | |
return; | |
} | |
// 再次檢查玩家是否仍然手持正確的物品。 | |
ItemStack currentMainHand = player.getInventory().getItemInMainHand(); | |
ItemStack currentOffHand = player.getInventory().getItemInOffHand(); | |
boolean currentHasRequiredItems = currentMainHand.getType() == Material.STONE_BUTTON && currentOffHand.getType() == Material.DISPENSER; | |
if (!currentHasRequiredItems) { | |
stopFiringTask(player.getUniqueId()); // 如果物品不對,停止任務。 | |
return; | |
} | |
ItemStack arrowStack = cachedArrowStacks.get(player.getUniqueId()); | |
// 如果緩存的箭矢堆疊為空或數量為0,則重新查找玩家背包。 | |
if (arrowStack == null || arrowStack.getAmount() == 0) { | |
arrowStack = findArrowInInventory(player); | |
cachedArrowStacks.put(player.getUniqueId(), arrowStack); // 更新緩存 | |
if (arrowStack == null) { | |
stopFiringTask(player.getUniqueId()); // 如果找不到箭矢,停止任務。 | |
return; | |
} | |
} | |
// 再次確保箭矢堆疊不為空且數量大于0,這是一個健壯性檢查。 | |
if (arrowStack.getAmount() <= 0) { | |
stopFiringTask(player.getUniqueId()); | |
return; | |
} | |
// 箭矢消耗邏輯:50% 幾率不消耗箭矢。 | |
if (random.nextDouble() < 0.5) { | |
// 不消耗箭矢 | |
} else { | |
arrowStack.setAmount(arrowStack.getAmount() - 1); // 消耗一支箭矢。 | |
} | |
// 在玩家眼睛位置發射箭矢,初始速度方向是玩家的視角方向,乘以6.0表示速度大小。 | |
Arrow arrow = player.launchProjectile(Arrow.class, player.getEyeLocation().getDirection().multiply(6.0)); | |
// 箭矢擴散邏輯:根據持續射擊時間增加擴散度。 | |
long timeElapsed = System.currentTimeMillis() - firingStartTime.getOrDefault(player.getUniqueId(), System.currentTimeMillis()); | |
// timeElapsed: 持續射擊的時間,單位毫秒。 | |
// getOrDefault: 如果找不到玩家的開始時間,則使用當前時間,避免空指針。 | |
// 最大擴散角度(弧度),例如0.5弧度約等于28度。 | |
double maxSpread = 0.5; | |
// 擴散因子:將持續時間歸一化到0-1之間,例如5秒(5000毫秒)達到最大擴散。 | |
double spreadFactor = Math.min(1.0, timeElapsed / 5000.0); | |
// 當前擴散量:最大擴散乘以擴散因子。 | |
double currentSpread = maxSpread * spreadFactor; | |
// 獲取玩家的基礎視角方向。 | |
Vector baseDirection = player.getLocation().getDirection(); | |
// 應用隨機擴散:通過在基礎方向上添加小的隨機偏移量來模擬擴散。 | |
// random.nextDouble() - 0.5: 生成-0.5到0.5之間的隨機數。 | |
// 乘以currentSpread來控制擴散的強度。 | |
double randomX = (random.nextDouble() - 0.5) * currentSpread; | |
double randomY = (random.nextDouble() - 0.5) * currentSpread; | |
double randomZ = (random.nextDouble() - 0.5) * currentSpread; | |
// 克隆基礎方向,然后加上隨機偏移量,最后歸一化以保持方向向量的單位長度。 | |
Vector spreadDirection = baseDirection.clone().add(new Vector(randomX, randomY, randomZ)).normalize(); | |
// 將新的擴散方向應用于箭矢的速度,速度大小保持不變。 | |
arrow.setVelocity(spreadDirection.multiply(6.0)); | |
arrow.setShooter(player); // 設置箭矢的射擊者為玩家,這樣箭矢的擊中事件可以追溯到玩家。 | |
// 播放射擊音效。 | |
player.playSound(player.getLocation(), Sound.ENTITY_ARROW_SHOOT, 1.0F, 1.0F); | |
}, 0L, 1L); // 0L延遲,1L周期,即每刻都執行。 | |
activeFiringTasks.put(player.getUniqueId(), task); // 將任務存儲到Map中,以便后續停止。 | |
} else { | |
// 如果玩家不再手持正確的物品,停止任何正在進行的射擊任務。 | |
stopFiringTask(player.getUniqueId()); | |
} | |
} | |
@EventHandler | |
public void onPlayerItemHeld(PlayerItemHeldEvent event) { | |
// 如果玩家切換了主手物品,停止射擊任務。 | |
stopFiringTask(event.getPlayer().getUniqueId()); | |
} | |
@EventHandler | |
public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) { | |
// 如果玩家交換了主副手物品,停止射擊任務。 | |
stopFiringTask(event.getPlayer().getUniqueId()); | |
} | |
@EventHandler | |
public void onPlayerQuit(PlayerQuitEvent event) { | |
// 如果玩家退出服務器,停止射擊任務,進行清理。 | |
stopFiringTask(event.getPlayer().getUniqueId()); | |
} | |
private void stopFiringTask(UUID playerId) { | |
// 這是一個私有輔助方法,用于停止指定玩家的射擊任務并清理相關數據。 | |
BukkitTask task = activeFiringTasks.remove(playerId); // 從Map中移除并獲取任務實例。 | |
firingStartTime.remove(playerId); // 移除開始時間。 | |
cachedArrowStacks.remove(playerId); // 移除緩存的箭矢堆疊。 | |
if (task != null) { | |
task.cancel(); // 如果任務存在,取消它,停止重復執行。 | |
} | |
} | |
private ItemStack findArrowInInventory(Player player) { | |
// 這是一個私有輔助方法,用于在玩家背包中查找箭矢。 | |
// 優先在快捷欄 (hotbar) 中查找箭矢 (索引 0-8)。 | |
for (int i = 0; i < 9; i++) { | |
ItemStack item = player.getInventory().getItem(i); | |
if (item != null && item.getType() == Material.ARROW) { | |
return item; // 找到即返回。 | |
} | |
} | |
// 如果快捷欄沒有,則檢查背包的其他部分。 | |
for (ItemStack item : player.getInventory().getContents()) { | |
if (item != null && item.getType() == Material.ARROW) { | |
return item; // 找到即返回。 | |
} | |
} | |
return null; // 如果整個背包都找不到箭矢,則返回null。 | |
} | |
} |
4.1 類定義與構造函數
public class ContinuousArrowFireListener implements Listener
:public
: 公共訪問修飾符,意味著這個類可以在任何地方被訪問。class
: 定義一個類。implements Listener
: Java中,一個類可以實現一個或多個接口。Listener
是一個Bukkit API接口,實現它表明這個類可以作為事件監聽器。- Python/C#對比: 類似于C#的?
public class MyListener : IListener
?或 Python中定義一個類,然后由框架在內部注冊其帶有特定裝飾器的方法。
- 成員變量:
private final RainyxinMAIN plugin;
:?private
表示私有,只能在類內部訪問。final
表示這個變量一旦被賦值就不能再改變。RainyxinMAIN
是你的主插件類,通過它我們可以訪問插件的配置、日志等。private final Map<UUID, BukkitTask> activeFiringTasks;
: 使用Map
(在Java中是HashMap
的接口)來存儲每個玩家對應的射擊任務。鍵是UUID
(玩家的唯一ID),值是BukkitTask
(Bukkit調度器返回的任務對象)。這樣,我們可以方便地根據玩家ID查找并取消他們的射擊任務。- Python/C#對比: 類似于Python的?
self.active_firing_tasks = {}
?或 C#的?private readonly Dictionary<Guid, Task> activeFiringTasks = new Dictionary<Guid, Task>();
。
- 構造函數?
public ContinuousArrowFireListener(RainyxinMAIN plugin)
:- 這是創建
ContinuousArrowFireListener
對象時執行的代碼。它接收主插件的實例作為參數,并將其保存到this.plugin
。 - 在構造函數中,所有
HashMap
都被初始化為空。
- 這是創建
4.2?onPlayerInteract
:玩家交互事件監聽
@EventHandler
: 這個注解告訴Bukkit的事件系統,onPlayerInteract
方法是一個事件處理程序。當PlayerInteractEvent
事件發生時,Bukkit會自動調用這個方法。event.getPlayer()
: 獲取觸發事件的Player
對象,代表了游戲中的玩家。event.getAction()
: 獲取玩家的交互動作,我們只關心RIGHT_CLICK_AIR
(右鍵空氣)和RIGHT_CLICK_BLOCK
(右鍵方塊)。- 權限檢查?
player.hasPermission("rainyxinmain.feature.continuousarrow")
: 這是一個很好的實踐,只允許擁有特定權限的玩家使用此功能。插件的plugin.yml
中需要定義這個權限。 - 物品檢查:
player.getInventory().getItemInMainHand()
?和?player.getInventory().getItemInOffHand()
:分別獲取玩家主手和副手持有的ItemStack
。mainHand.getType() == Material.STONE_BUTTON
?和?offHand.getType() == Material.DISPENSER
: 檢查物品的類型是否符合要求。Material
是一個枚舉,包含了Minecraft中所有物品和方塊的類型。
4.3 核心:Bukkit調度器與持續射擊任務
activeFiringTasks.containsKey(player.getUniqueId())
: 在啟動新任務之前,檢查玩家是否已經有一個活躍的射擊任務。這可以防止玩家多次右鍵時啟動多個重復的任務。firingStartTime.put(player.getUniqueId(), System.currentTimeMillis());
: 記錄玩家開始射擊的當前系統時間(毫秒)。這用于后續計算射擊的持續時間,從而影響箭矢的擴散。BukkitTask task = Bukkit.getScheduler().runTaskTimer(plugin, () -> { ... }, 0L, 1L);
: 這是實現持續射擊的核心。Bukkit.getScheduler()
: 獲取Bukkit的調度器實例。runTaskTimer(...)
: 計劃一個重復執行的任務。plugin
: 你的主插件實例,告訴Bukkit這個任務屬于哪個插件。() -> { ... }
: 這是一個Lambda表達式,它定義了任務在每次執行時要運行的代碼塊。在Java中,這通常用于實現Runnable
接口,類似于Python的匿名函數或C#的匿名方法/Lambda表達式。0L
: 第一次執行任務前的延遲(0個游戲刻)。L
表示這是一個long
類型的值。1L
: 任務重復的周期(每1個游戲刻執行一次)。Minecraft每秒有20個游戲刻,所以這意味著每0.05秒發射一支箭矢,實現了“機槍”的效果。
- Lambda內部邏輯:
- 在線檢查和物品檢查: 每刻都再次檢查玩家是否在線,以及是否仍然手持正確的物品。如果條件不再滿足,就調用
stopFiringTask
停止任務。這是保持任務健壯性和響應性的關鍵。 - 箭矢查找與緩存:?
cachedArrowStacks.get(player.getUniqueId())
嘗試獲取緩存的箭矢。如果緩存為空或箭矢用完,會調用findArrowInInventory
重新查找。這樣做可以減少頻繁遍歷玩家背包的開銷。 - 箭矢消耗:?
arrowStack.setAmount(arrowStack.getAmount() - 1);
?將箭矢數量減少1。 random.nextDouble() < 0.5
:?random.nextDouble()
生成一個0.0到1.0之間的隨機浮點數。如果小于0.5(即有50%的幾率),就不消耗箭矢。
- 在線檢查和物品檢查: 每刻都再次檢查玩家是否在線,以及是否仍然手持正確的物品。如果條件不再滿足,就調用
activeFiringTasks.put(player.getUniqueId(), task);
: 將新創建的任務對象存儲在activeFiringTasks
?Map中,以玩家的UUID作為鍵。這樣,我們就可以在玩家改變物品或退出時,通過UUID找到并取消這個任務。
4.4 箭矢生成、消耗與擴散邏輯
Arrow arrow = player.launchProjectile(Arrow.class, player.getEyeLocation().getDirection().multiply(6.0));
:player.launchProjectile(Arrow.class, ...)
: Bukkit提供的方法,用于在玩家位置發射一個指定類型的投擲物。Arrow.class
指定了投擲物是箭矢。player.getEyeLocation().getDirection()
: 獲取玩家視角的方向向量。.multiply(6.0)
: 將方向向量乘以6.0,設置箭矢的初始速度大小。
- 箭矢擴散?
spread
?邏輯: 這是這個功能的一個亮點,模擬了機槍射擊越久越不準的效果。long timeElapsed = System.currentTimeMillis() - firingStartTime.getOrDefault(player.getUniqueId(), System.currentTimeMillis());
: 計算從開始射擊到當前時間經過了多少毫秒。getOrDefault
是為了防止firingStartTime
中沒有該玩家的記錄(雖然理論上不會發生)。double maxSpread = 0.5;
: 定義了最大的擴散角度(單位是弧度)。可以調整這個值來控制擴散程度。double spreadFactor = Math.min(1.0, timeElapsed / 5000.0);
: 計算擴散因子。將timeElapsed
除以5000.0(5秒),并用Math.min(1.0, ...)
確保因子不會超過1.0。這意味著在持續射擊5秒后,擴散達到最大。double currentSpread = maxSpread * spreadFactor;
: 實際的擴散量,隨著時間逐漸增大。Vector baseDirection = player.getLocation().getDirection();
: 獲取玩家當前的朝向。randomX/Y/Z
: 通過在-0.5 * currentSpread
到0.5 * currentSpread
之間生成隨機數,來為箭矢的飛行方向添加隨機擾動。baseDirection.clone().add(new Vector(randomX, randomY, randomZ)).normalize();
:.clone()
: 創建baseDirection
的副本,避免修改原始的玩家方向。.add(new Vector(...))
: 將隨機偏移量加到基礎方向上。.normalize()
: 將結果向量歸一化,使其長度為1,只保留方向信息。
arrow.setVelocity(spreadDirection.multiply(6.0));
: 將計算出的帶有擴散的spreadDirection
應用到箭矢的速度上,速度大小保持不變。
arrow.setShooter(player);
: 這很重要!它將玩家設置為箭矢的射擊者。這意味著如果箭矢擊中生物,游戲會認為是由該玩家造成的傷害,并且其他插件(如領地插件)也可以正確識別箭矢來源。player.playSound(...)
: 播放一個射擊音效。Sound.ENTITY_ARROW_SHOOT
是Bukkit提供的內置音效。參數分別是位置、音量和音高。
4.5 停止射擊的條件與清理
為了確保資源被正確釋放,并且功能在玩家不再符合條件時停止,有幾個事件處理器來處理停止射擊的邏輯:
onPlayerItemHeld(PlayerItemHeldEvent event)
: 當玩家切換快捷欄物品時觸發。如果玩家切換了手持物品,機槍就應該停止射擊。onPlayerSwapHandItems(PlayerSwapHandItemsEvent event)
: 當玩家使用快捷鍵交換主副手物品時觸發。onPlayerQuit(PlayerQuitEvent event)
: 當玩家退出服務器時觸發。必須停止任務,否則可能會導致內存泄漏或其他問題。private void stopFiringTask(UUID playerId)
:- 這是一個私有輔助方法,用于集中處理停止任務的邏輯。
activeFiringTasks.remove(playerId)
: 從Map中移除玩家對應的任務。firingStartTime.remove(playerId)
?和?cachedArrowStacks.remove(playerId)
: 清理與該玩家相關的其他緩存數據。task.cancel()
:?關鍵一步。調用BukkitTask
的cancel()
方法會停止由runTaskTimer
創建的重復任務,防止它繼續執行。
4.6 輔助方法:查找箭矢
private ItemStack findArrowInInventory(Player player)
:- 這個方法用于在玩家的背包中查找箭矢。
- 優先檢查快捷欄:?
for (int i = 0; i < 9; i++)
?循環檢查玩家背包的前9個槽位(即快捷欄)。 - 檢查整個背包: 如果快捷欄沒有找到,再遍歷
player.getInventory().getContents()
檢查所有背包槽位。 item != null && item.getType() == Material.ARROW
: 檢查槽位是否有物品,并且物品類型是否是箭矢。
5. 構建、部署與測試
-
項目創建 (IntelliJ IDEA):
- 打開IntelliJ IDEA。
- 選擇?
New Project
。 - 選擇?
Maven
。 - 選擇?
Create from Archetype
,然后點擊?Add Archetype
。GroupId
:?org.bukkit
ArtifactId
:?bukkit-archetype
Version
:?1.0.1-SNAPSHOT
?(或者更高的穩定版本)
- 填寫?
GroupId
?(如?com.rainyxinmain
),?ArtifactId
?(如?rainyxinmain
)。 - 完成項目創建向導。
- 手動配置: 很多時候,直接使用Maven Archetype可能會引入舊版本的Bukkit或不適用于PaperMC。更常見的方式是:
- 創建新的Maven項目。
- 手動添加上述3.3節中的
pom.xml
內容。 - 創建你的主插件類 (
RainyxinMAIN.java
),繼承JavaPlugin
。 - 創建?
resources
?文件夾并在其中創建?plugin.yml
?文件。
-
集成代碼:
- 將
ContinuousArrowFireListener.java
文件放到正確的包路徑下(例如:src/main/java/com/rainyxinmain/rainyxinmain/features/
)。 - 確保你的主插件類?
RainyxinMAIN.java
?中,在?onEnable()
?方法內注冊了監聽器:// ... 在 RainyxinMAIN.java 中
@Override
public void onEnable() {
// 注冊 ContinuousArrowFireListener
getServer().getPluginManager().registerEvents(new ContinuousArrowFireListener(this), this);
getLogger().info("RainyXinMain features are enabled!");
}
// ...
- 將
-
構建插件:
- 在IntelliJ IDEA中,打開Maven工具窗口 (通常在右側)。
- 在?
rainyxinmain
?->?Lifecycle
?下,雙擊?clean
,然后雙擊?package
。 - Maven會下載依賴、編譯代碼,并生成一個JAR文件(通常在?
target/
?目錄下,名為?rainyxinmain-1.0-SNAPSHOT.jar
)。
-
部署到服務器:
- 將生成的JAR文件復制到你的Minecraft服務器根目錄下的?
plugins
?文件夾中。 - 啟動或重啟你的Minecraft服務器。
- 將生成的JAR文件復制到你的Minecraft服務器根目錄下的?
-
測試功能:
- 進入游戲,成為OP (
/op <你的ID>
)。 - 給予自己權限 (
/lp user <你的ID> permission set rainyxinmain.feature.continuousarrow true
)。 - 通過命令獲取物品:
/give @s stone_button
/give @s dispenser
/give @s arrow 64
- 主手持有石頭按鈕,副手持有發射器。
- 右鍵!你應該能看到箭矢像機槍一樣發射出來,并且隨著射擊時間的增加,箭矢會越來越散。
- 嘗試切換手持物品或退出游戲,檢查機槍是否停止射擊。
- 進入游戲,成為OP (
6. 擴展與進階
- 配置化: 將物品類型、射速、擴散參數、箭矢消耗幾率等變量寫入插件的配置文件 (
config.yml
),允許服務器管理員自定義。 - 不同物品組合: 允許更多物品組合來觸發不同的射擊模式(例如,使用弓+TNT可以發射爆炸箭)。
- 冷卻時間: 添加射擊冷卻時間,防止過于頻繁的啟動。
- 效果與粒子: 在射擊時添加粒子效果或更多音效。
- 自定義箭矢: 為發射的箭矢添加自定義屬性,例如火焰箭、毒箭等。
- 動畫: 模擬發射器的發射動畫。
- 重構: 將箭矢消耗、擴散計算等邏輯封裝到單獨的輔助類中,使代碼更模塊化。
- CommandAPI/PaperAPI: 學習使用更高級的API,如PaperMC提供的額外API,或者CommandAPI簡化命令創建。
- 數據庫集成: 存儲玩家的自定義設置或統計數據。
7. 總結
通過這個“手持發射器箭矢機槍”的例子,你已經:
- 了解了Java語言與Python/C#的相似點和不同點。
- 掌握了Bukkit事件、監聽器和調度器的核心概念。
- 學會了如何設置Maven項目和
plugin.yml
。 - 親手分析并理解了一個實際的Minecraft插件功能代碼。
- 實踐了插件的構建、部署和測試。