Back to Blog

Building a Custom Minecraft Plugin

MinecraftJavaTutorialPlugin DevelopmentSpigot API
Person98Person98
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:

  1. Create a new Maven project in your IDE
  2. 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

  1. Build your plugin:
mvn clean package
  1. Find the JAR file in target/MyFirstPlugin-1.0.jar
  2. Copy it to your test server's plugins folder
  3. Start the server and check the console for any errors
  4. Test your commands and features in-game

Common Pitfalls and Best Practices

  1. Performance Considerations

    • Use async tasks for heavy operations
    • Cache frequently accessed data
    • Avoid excessive event listening
  2. Error Handling

    • Always validate user input
    • Use try-catch blocks for risky operations
    • Log errors properly for debugging
  3. 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!