面向Python/C#開發者入門Java與Bukkit API

本教程將以"手持發射器箭矢機槍"功能為例,帶你掌握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:?換行。
    • 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關鍵字是必須的)。
  • 訪問修飾符:?public,?private,?protected,?default?(包級私有)。
    • Python:?_name?(約定私有),?__name?(名稱修飾)。
    • C#/Java:?public?(公開),?private?(私有),?protected?(受保護的)。
  • 接口 (Interface):
    • C#:?interface IMyInterface { void DoSomething(); }.
    • Java:?interface MyInterface { void doSomething(); }?(與C#非常相似,類實現接口使用implements關鍵字)。
  • 泛型 (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?(在函數式接口上下文中使用,如RunnableConsumer等)。

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.xmlplugin.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 * currentSpread0.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():?關鍵一步。調用BukkitTaskcancel()方法會停止由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. 構建、部署與測試

  1. 項目創建 (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?文件。
  2. 集成代碼:

    • 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!");
      }
      // ...
  3. 構建插件:

    • 在IntelliJ IDEA中,打開Maven工具窗口 (通常在右側)。
    • 在?rainyxinmain?->?Lifecycle?下,雙擊?clean,然后雙擊?package
    • Maven會下載依賴、編譯代碼,并生成一個JAR文件(通常在?target/?目錄下,名為?rainyxinmain-1.0-SNAPSHOT.jar)。
  4. 部署到服務器:

    • 將生成的JAR文件復制到你的Minecraft服務器根目錄下的?plugins?文件夾中。
    • 啟動或重啟你的Minecraft服務器。
  5. 測試功能:

    • 進入游戲,成為OP (/op <你的ID>)。
    • 給予自己權限 (/lp user <你的ID> permission set rainyxinmain.feature.continuousarrow true)。
    • 通過命令獲取物品:
      • /give @s stone_button
      • /give @s dispenser
      • /give @s arrow 64
    • 主手持有石頭按鈕,副手持有發射器。
    • 右鍵!你應該能看到箭矢像機槍一樣發射出來,并且隨著射擊時間的增加,箭矢會越來越散。
    • 嘗試切換手持物品或退出游戲,檢查機槍是否停止射擊。

6. 擴展與進階

  • 配置化: 將物品類型、射速、擴散參數、箭矢消耗幾率等變量寫入插件的配置文件 (config.yml),允許服務器管理員自定義。
  • 不同物品組合: 允許更多物品組合來觸發不同的射擊模式(例如,使用弓+TNT可以發射爆炸箭)。
  • 冷卻時間: 添加射擊冷卻時間,防止過于頻繁的啟動。
  • 效果與粒子: 在射擊時添加粒子效果或更多音效。
  • 自定義箭矢: 為發射的箭矢添加自定義屬性,例如火焰箭、毒箭等。
  • 動畫: 模擬發射器的發射動畫。
  • 重構: 將箭矢消耗、擴散計算等邏輯封裝到單獨的輔助類中,使代碼更模塊化。
  • CommandAPI/PaperAPI: 學習使用更高級的API,如PaperMC提供的額外API,或者CommandAPI簡化命令創建。
  • 數據庫集成: 存儲玩家的自定義設置或統計數據。

7. 總結

通過這個“手持發射器箭矢機槍”的例子,你已經:

  • 了解了Java語言與Python/C#的相似點和不同點。
  • 掌握了Bukkit事件、監聽器和調度器的核心概念。
  • 學會了如何設置Maven項目和plugin.yml
  • 親手分析并理解了一個實際的Minecraft插件功能代碼。
  • 實踐了插件的構建、部署和測試。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/93138.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/93138.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/93138.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

從100到0.3美元:GPT-5用價格戰血洗大模型賽道

————————— 一、從 100 美元到 0.3 美元&#xff1a;史無前例的效率革命 ————————— 互聯網女王 Mary Meeker 在《AI 趨勢報告 2025》里寫下這組數字&#xff1a; ? 訓練成本 8 年飆升 2400 倍&#xff1b; ? 推理成本 2 年暴跌 99.7%。OpenAI 把“暴跌”推到…

第三十二天(文件操作安全)

文件遍歷上傳下載刪除編輯包含等 $_FILES&#xff1a;PHP中一個預定義的超全局變量&#xff0c;用于在上傳文件時從客戶端接收文件&#xff0c;并將其保存到服務器上。它是一個包含上傳文件信息的數組&#xff0c;包括文件名、類型、大小、臨時文件名等信息。 $_FILES"表…

系統集成項目管理工程師【第十一章 規劃過程組】規劃風險應對、規劃采購管理篇

系統集成項目管理工程師【第十一章 規劃過程組】規劃風險應對、規劃采購管理篇 一、規劃風險應對&#xff1a;為項目穿上"防護衣" 1. 什么是規劃風險應對&#xff1f; 規劃風險應對是基于風險量化分析結果&#xff0c;制定可選方案、選擇應對策略并商定具體行動的過程…

20250813比賽總結

題目T1.volumeT2.storyT3.treeT4.game預計分數6060030實際分數306000T1.volume 確實是暴力&#xff0c;但我是用數組統計每個可能出現的數&#xff0c;于是3AC 3WA 4TLE。拿到全部分應該直接按照題目模擬。 T2.story 暴力dfs&#xff0c;由于忘記優化所以60pts&#xff0c;而且…

適合物流/應急/工業的對講機,AORO M6 Pro構建高效指揮調度方案

在物流調度、應急救援與工業協同等對通信可靠性要求極高的領域中&#xff0c;專業對講設備的技術迭代直接關系到任務執行效率與安全保障。AORO M6 Pro對講機作為新一代融合通信終端&#xff0c;正以多模融合技術與國產化自主創新&#xff0c;為復雜場景下的高效調度提供堅實的技…

類和對象----中

這里寫目錄標題<font color"#FF00FF">1. 類和對象(中)<font color"#FF00FF">2. 構造函數<font color"#FF00FF">3. 析構函數<font color"#FF00FF">4. 拷?構造函數1. 類和對象(中) 類的默認成員函數&#xff1…

CAD 的 C# 開發中,對多段線(封閉多邊形)內部的點進行 “一筆連線且不交叉、不出界

本質上是約束條件下的路徑規劃問題&#xff0c;核心是找到一條連續路徑遍歷所有點&#xff0c;同時滿足&#xff1a; 路徑不與自身交叉&#xff1b; 路徑全程在多段線&#xff08;多邊形&#xff09;內部&#xff1b; 路徑連續&#xff08;一筆畫&#xff09;。核心思路與算法步…

ZED 2i相機調試

1. 測試 ZED SDK /usr/local/zed/tools/ZED_Diagnostic/usr/local/zed/tools/ZED_Explorer2. 安裝SDK How to Install ZED SDK on Linux - Stereolabs 安裝命令&#xff1a; sudo apt install zstd./ZED_SDK_Ubuntu20_cuda12.1_tensorrt8.6_v5.0.5.zstd.run

Go語言select并發編程實戰指南

一、select作用Go 語言中的 select 語句是處理多通道&#xff08;Channel&#xff09;操作的核心控制結構&#xff0c;專為高效并發通信而設計。通過巧妙運用 select 語句&#xff0c;開發者能夠高效實現并發控制、超時處理和非阻塞通信等功能&#xff0c;使其成為 Go 語言并發…

OpenCV常見問題匯總

1、深度拷貝的問題我對整張圖像通過裁剪分別進行識別&#xff0c;出現識別結果與期望不同的問題&#xff0c;經過大量排查是OpenCV深度拷貝問題&#xff0c;我原來有問題的寫法cv::Mat matCrop matZoom(roi); cv::Mat matCrop1 matCrop(roi1); cv::Mat matCrop2 matCrop(roi2)…

【Unity開發】Unity核心學習(一)

一、2D相關1、圖片導入相關設置 &#xff08;1&#xff09;Unity支持的圖片格式 支持BMP、TIF、JPG、PNG、TGA、PSD 常用格式具體介紹&#xff1a; JPG&#xff1a;指JPGE格式&#xff0c;屬于有損壓縮格式&#xff0c;無透明通道 PNG&#xff1a;無損壓縮格式&#xff0c;有透…

Python自定義異常類的寫法與使用場景

在軟件開發的生命周期中&#xff0c;異常處理是保障程序健壯性與可維護性的關鍵環節。Python作為一門高級編程語言&#xff0c;內置了豐富的異常機制&#xff0c;能夠高效、優雅地應對運行時的各種錯誤。然而&#xff0c;面對復雜業務場景和多層架構時&#xff0c;內置異常往往…

為 Promethus 配置https訪問

一、序言 本篇將介紹如何使用數字證書為Promethus 訪問提供加密功能&#xff0c;由于是實驗環境證書由openssl生成&#xff0c;操作指南來自官網手冊&#xff1a;https://prometheus.io/docs/guides/tls-encryption/在生產環境中prometheus可能會放在后端&#xff0c;證書一般配…

擺脫例行 SQL 報表的隱性成本:用 n8n 構建四節點自動化報告流程

例行 SQL 報表的隱藏成本 各類組織的數據團隊都面臨同樣的反復難題:利益相關方需要定期報告,但手工 SQL 報表占用了本可用于分析的寶貴時間。無論公司規模如何,流程幾乎一致——連接數據庫、執行查詢、格式化結果,并將結論分發給決策者。 數據從業者經常要處理并不需要高…

HCIP——OSPF綜合實驗

一、實驗拓撲二、實驗要求1、R4為ISP&#xff0c;其上只配置IP地址&#xff1b;R4與其他所直連設備間均使用公有IP&#xff1b; 2、R3-R5、R6、R7為MGRE環境&#xff0c;R3為中心站點&#xff1b; 3、整個OSPF環境IP基于172.16.0.0/16劃分&#xff1b;除了R12有兩個環回&#x…

GitHub 趨勢日報 (2025年08月12日)

&#x1f4ca; 由 TrendForge 系統生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日報中的項目描述已自動翻譯為中文 &#x1f4c8; 今日獲星趨勢圖 今日獲星趨勢圖1397gpt4all442system-prompts-and-models-of-ai-tools331umami307full-stack-fast…

Linux網絡性能調優終極指南:深度解析與實踐

Linux網絡性能調優終極指南&#xff1a;深度解析與實踐 一、性能調優核心原理體系 1.1 數據包生命周期與性能瓶頸 #mermaid-svg-TsvnmiGx1WeTerK2 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-TsvnmiGx1WeTerK2 .…

串口超時參數深度解析:ReadTotalTimeoutMultiplier、ReadIntervalTimeout等

一、參數定義與作用 1.1 ReadIntervalTimeout&#xff08;字符間隔超時&#xff09; 定義&#xff1a;指定兩個連續字符到達之間的最大允許時間&#xff08;毫秒&#xff09;作用&#xff1a;當接收兩個字符的時間間隔超過該值時&#xff0c;ReadFile操作立即返回已緩沖的數據特…

ubuntu20.04下C++實現點云的多邊形區域過濾(2種實現:1、pcl的CropHull濾波器;2、CUDA上實現射線法)

在點云目標檢測中&#xff0c;經常會有一系列的誤識別&#xff0c;為了減小誤識別的概率&#xff0c;可以通過區域過濾來刪除不需要的點云&#xff0c;如下圖所示 本例中點云的場景為路口交通場景&#xff0c;已經把雷達坐標系的xoy面轉換至點云中的地平面&#xff0c;具體原理…

Java 大視界 -- Java 大數據在智能家居場景聯動與用戶行為模式挖掘中的應用(389)

Java 大視界 -- Java 大數據在智能家居場景聯動與用戶行為模式挖掘中的應用(389) 引言: 正文: 一、傳統智能家居的 “劇本困境”:按流程走,不管人需 1.1 設備與用戶的 “理解差” 1.1.1 場景聯動 “太機械” 1.1.2 行為識別 “太粗糙” 1.1.3 技術落地的 “體驗坑” 二、…