Building a Custom Minecraft Plugin
Building a Custom Minecraft Plugin
Introduction
Minecraft plugins are powerful tools that can transform a vanilla server into a unique gaming experience. Whether you want to add custom commands, new gameplay mechanics, or server management features, plugin development opens up endless possibilities. In this guide, we'll create a practical plugin from the ground up using the Spigot API.
Prerequisites
Before diving in, you'll need:
- Java Development Kit (JDK) 17 or newer
- An Integrated Development Environment (IDE) - IntelliJ IDEA recommended
- Basic Java programming knowledge
- A test server running Spigot/Paper
- Maven for dependency management
Project Setup
Let's start by creating a proper development environment:
- Create a new Maven project in your IDE
- Configure your
pom.xml
with the Spigot API dependency:
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>\n<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
Essential Plugin Files
Every Spigot plugin requires two key files:
1. plugin.yml
Create this file in src/main/resources
:
name: MyFirstPlugin
version: '1.0'
main: com.example.myfirstplugin.MyFirstPlugin
api-version: '1.20'
commands:
heal:
description: Heals a player to full health
usage: /<command> [player]
2. Main Plugin Class
Create your main class in src/main/java/com/example/myfirstplugin/MyFirstPlugin.java
:
package com.example.myfirstplugin;
import org.bukkit.plugin.java.JavaPlugin;
public class MyFirstPlugin extends JavaPlugin {
@Override
public void onEnable() {
// Load configuration
saveDefaultConfig();
// Register commands
getCommand("heal").setExecutor(new HealCommand(this));
// Register event listeners
getServer().getPluginManager().registerEvents(new PlayerEvents(this), this);
getLogger().info("MyFirstPlugin has been enabled!");
}
@Override
public void onDisable() {
getLogger().info("MyFirstPlugin has been disabled!");
}
}
Creating Practical Features
Let's implement some useful functionality:
1. Healing Command
public class HealCommand implements CommandExecutor {
private final MyFirstPlugin plugin;
public HealCommand(MyFirstPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player)) {
sender.sendMessage("This command can only be used by players!");
return true;
}
Player player = (Player) sender;
if (!player.hasPermission("myfirstplugin.heal")) {
player.sendMessage(ChatColor.RED + "You don't have permission to use this command!");
return true;
}
player.setHealth(player.getMaxHealth());
player.sendMessage(ChatColor.GREEN + "You have been healed!");
return true;
}
}
2. Player Events
public class PlayerEvents implements Listener {
private final MyFirstPlugin plugin;
public PlayerEvents(MyFirstPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
// Custom join message
event.setJoinMessage(ChatColor.YELLOW + "Welcome, " +
player.getName() + " to the server!");
// Give starter items
if (!player.hasPlayedBefore()) {
player.getInventory().addItem(
new ItemStack(Material.BREAD, 16),
new ItemStack(Material.WOODEN_SWORD, 1)
);
}
}
}
Configuration Management
Create a default config.yml
in your resources folder:
messages:
welcome: "&eWelcome to our server, %player%!"
heal-success: "&aYou have been healed!"
features:
give-starter-items: true
starter-items:
- BREAD:16
- WOODEN_SWORD:1
Building and Testing
- Build your plugin:
mvn clean package
- Find the JAR file in
target/MyFirstPlugin-1.0.jar
- Copy it to your test server's
plugins
folder - Start the server and check the console for any errors
- Test your commands and features in-game
Common Pitfalls and Best Practices
-
Performance Considerations
- Use async tasks for heavy operations
- Cache frequently accessed data
- Avoid excessive event listening
-
Error Handling
- Always validate user input
- Use try-catch blocks for risky operations
- Log errors properly for debugging
-
Resource Management
- Clean up resources in onDisable()
- Use proper memory management
- Close database connections
Advanced Features
Custom Items with Events
Creating custom items with special abilities can make your plugin more engaging. Here's how to create a "Thunder Sword" that strikes lightning when hitting enemies:
public class ThunderSword {
public static ItemStack create() {
ItemStack sword = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta meta = sword.getItemMeta();
meta.setDisplayName(ChatColor.BLUE + "Thunder Sword");
meta.setLore(Arrays.asList(
ChatColor.GRAY + "Strike your enemies with lightning!",
ChatColor.YELLOW + "Right-click to summon a thunderstorm"
));
// Add custom NBT tag to identify the item
meta.getPersistentDataContainer().set(
new NamespacedKey(plugin, "thunder-sword"),
PersistentDataType.BOOLEAN,
true
);
sword.setItemMeta(meta);
return sword;
}
}
@EventHandler
public void onEntityDamage(EntityDamageByEntityEvent event) {
if (!(event.getDamager() instanceof Player)) return;
Player player = (Player) event.getDamager();
ItemStack item = player.getInventory().getItemInMainHand();
if (isThunderSword(item)) {
Location strikeLocation = event.getEntity().getLocation();
strikeLocation.getWorld().strikeLightning(strikeLocation);
player.getWorld().playSound(player.getLocation(), Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1.0f, 1.0f);
}
}
Optimization and Scalability
1. Efficient Data Storage
When dealing with player data, use appropriate storage solutions:
public class PlayerDataManager {
private final Map<UUID, PlayerData> playerCache = new HashMap<>();
private final MyFirstPlugin plugin;
private final FileConfiguration config;
public PlayerDataManager(MyFirstPlugin plugin) {
this.plugin = plugin;
this.config = plugin.getConfig();
}
public void savePlayerData(Player player) {
// Run async to prevent server lag
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
PlayerData data = playerCache.get(player.getUniqueId());
if (data != null) {
config.set("players." + player.getUniqueId() + ".kills", data.getKills());
config.set("players." + player.getUniqueId() + ".deaths", data.getDeaths());
plugin.saveConfig();
}
});
}
public void loadPlayerData(Player player) {
PlayerData data = new PlayerData();
data.setKills(config.getInt("players." + player.getUniqueId() + ".kills", 0));
data.setDeaths(config.getInt("players." + player.getUniqueId() + ".deaths", 0));
playerCache.put(player.getUniqueId(), data);
}
}
2. Performance Monitoring
Add a simple performance monitoring system:
public class PerformanceMonitor {
private final MyFirstPlugin plugin;
private int taskId;
private long lastCheck = System.currentTimeMillis();
private double tps = 20.0;
public PerformanceMonitor(MyFirstPlugin plugin) {
this.plugin = plugin;
startMonitoring();
}
private void startMonitoring() {
taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, () -> {
long now = System.currentTimeMillis();
long diff = now - lastCheck;
tps = 1000.0D / diff * 20.0D;
lastCheck = now;
if (tps < 18.0) {
plugin.getLogger().warning("Server TPS dropped to " + String.format("%.2f", tps));
// Implement automatic performance improvements here
}
}, 20L, 20L);
}
public double getCurrentTPS() {
return Math.min(20.0, tps);
}
}
Once you've mastered the basics, consider exploring:
- Custom inventories (GUIs)
- Database integration
- Particle effects and custom items
- Command tab completion
- Plugin configuration APIs
- Custom events and listeners
Conclusion
Building Minecraft plugins is an excellent way to learn Java while creating something fun and useful. Start small, test thoroughly, and gradually add more complex features as you become comfortable with the API. Remember to check the Spigot Documentation and JavaDocs for detailed reference.
Happy coding!