[Tutorial] Custom Entities - Meteor

Discussion in 'Resources' started by Icyene, Aug 16, 2012.

Thread Status:
Not open for further replies.
  1. Offline

    Icyene

    This resource is not kept up to date with every release of Minecraft, as it relies heavily on volatile internal Minecraft code. Figuring out the latest method mappings are left as an exercise for the reader.

    By the end of this tutorial, you'll be able to create an entity that can cause this:

    [​IMG]
    With minimal lag.

    So without further ado, lets begin!

    This tutorial assumes you have a basic understanding of NMS entities.

    Now, we don't need to create our own entity, entity physics, entity AI and all: we can piggy back on the default capabilities of a fireball.

    We now have to create our class, EntityMeteor for this tutorial, and make it extend EntityFireball. This will give us all the properties of a fireball. Your class should look something like this:

    Code:Java
    1.  
    2. public class EntityMeteor extends EntityFireball {
    3. public EntityMeteor(World world) {
    4. super(world);
    5. }
    6. }
    7.  


    We let the fireball perform all the required initialization.

    Since we've control over the logic of the fireball, we can do a few things to spice our meteor up: we can add a speed modifier (so that meteors accelerate, similar to how fireballs decelerate), and, of course, a section to create a trail of explosions for the meteor. We likely also want to form a crater after impact. For this end we can initialize three variables that we'll need.

    Code:Java
    1.  
    2. private float speedModifier = 1.10F; // How much to accelerate the meteor by each update
    3. private float trailPower = 10F; // The power of the spawned trail explosions
    4. private float impactPower = 50F; // The power of the final explosion
    5.  


    Now, we need to actually modify how the fireball behaves: we need to add some hooks to our entity. By analyzing EntityFireball (located in the net.minecraft.server package of CraftBukkit) with MCP, we find that the method h_ in EntityFireball is called whenever the fireball moves, similar to a PlayerMoveEvent. That is where we shall create our explosion, so we ought to override it.

    Code:Java
    1.  
    2. @Override
    3. public void h_() {
    4. // Perform logic on movement here
    5. super.h_(); // Delegate to original fireball code; run physics &c
    6. }
    7.  


    In our move function, we now need to spawn an explosion. We can retrieve a Bukkit location object from the Bukkit mirror of the fireball, and use it to spawn an explosion at the current position of trailPower force.

    Code:Java
    1.  
    2. Fireball fireball = (Fireball) this.getBukkitEntity();
    3. fireball.getWorld().createExplosion(fireball.getLocation(), trailPower);
    4.  


    Depending on your target server architecture, you may wish to set up an accumulator so that you only spawn an explosion every other (or less) call of the move method. This may be a wise choice - the entities position is updated once a tick (20 times/second under optimal conditions).

    Having created a trail, we now want to complete the second goal: making the meteor speed up. At our disposal we have three variables: motX, motY and motZ. As their name suggests, they together make up a velocity vector. Hence, we need only multiple them by our speedModifier to accelerate our meteors.

    Code:Java
    1.  
    2. motX *= speedModifier;
    3. motY *= speedModifier;
    4. motZ *= speedModifier;
    5.  



    Time for the impact explosion. Once again referencing MCP, we find that the method a is called upon impact. We need just override it and use the same explosion method as we did when generating the trail.

    Code:Java
    1.  
    2. @Override
    3. public void a(MovingObjectPosition movingobjectposition) {
    4. Fireball fireball = (Fireball) this.getBukkitEntity();
    5. fireball.getWorld().createExplosion(fireball.getLocation(), impactPower);
    6. super.a(movingobjectposition); // Perform cleanup, event firing and death
    7. }
    8.  


    If you look at the approach above, it may occur to you that by removing the call to the superclass a can make your fireball indestructible. You'd be right. If you wish, you may implement a burrowing system by allowing a to be called for a number of times without delegating to the superclass: the fireball will hit the ground, cause an explosion to burrow, and continue descending.

    Finally, we need to tell Minecraft that we have a new entity. If this is your first time creating a custom entity, I suggest you read this tutorial by Jacek - it explains things quite nicely.
    A simplistic approach to patching the meteor:

    Code:Java
    1.  
    2. public static void patch() {
    3. try {
    4. Method a = net.minecraft.server.EntityTypes.class.getDeclaredMethod("a", Class.class, String.class, int.class);
    5. a.setAccessible(true);
    6. a.invoke(a, EntityMeteor.class, "Meteor", 12);
    7. } catch (Exception ignored) {
    8. // Do some cleanup and error-handling here.
    9. }
    10. }
    11.  


    And, of course, all that remains is spawning the meteor into our world. This will be familiar for anyone with any NMS experience.

    Code:Java
    1.  
    2. CraftWorld handle = (CraftWorld) targetWorld;
    3. EntityMeteor meteor= new EntityMeteor(handle .getHandle());
    4. handle .getHandle().addEntity(meteor, SpawnReason.NATURAL);
    5. meteor.setPosition(x, y, z);
    6.  


    And that's it!

    Enhancements
    1. Fiery explosions

      Depending on use case, you may also want the fire effect caused by regular fireballs. At the time of writing, there is no way to do this from the Bukkit API. However, there exists a simple method to suit our needs.
    Code:Java
    1.  
    2. world.createExplosion(
    3. this, // This fireball
    4. locX, // X position of fireball
    5. locY, // Y position of fireball
    6. locZ, // Z position of fireball
    7. power, // Explosion power
    8. hasFire); // Flag for whether or not fire should be spawned.
    9.  
     
  2. Offline

    one4me

    Imagine using this to make meteor showers a type of Minecraft weather. If someone did that they may want to turn the explosions off and just leave the damage if that's possible so that the entire landscape doesn't get griefed. And as long as multiple meteors don't cause lag, it would be amazing.
     
  3. Offline

    Icyene

    one4me

    This code was pulled from my plugin Storm, which already has meteors. Meteor showers too are planned.
     
    one4me likes this.
  4. Offline

    Jacek

    Awesome ! Nice to see someone did somethign cool with this method :p
     
  5. Offline

    Icyene

    Thanks :p I also noticed some very strange occurrences with 1.3.1, which occur of either one of 2 things.

    1. Once you apply the patch, it is never needed to be reapplied, not even if server restarts.
    2. You do not require the patch to add an entity anymore.

    Because for quite a while I was forgetting to patch it, and my entities were will spawning. Maybe the patch makes them visible? That might explain it: unpatched entities are not visible, but still update positions etc. Because I almost never see the actual fireball of the meteor, I always see it's trail.

    Food for thought :p
     
    Jacek likes this.
  6. Offline

    Jacek

    Yeah if you don't update the class reference thing they are invisible. It's really odd that that happens though, it should just give loads of errors really :s It could be that it sends the wrong entity id to the client so there is no skin to use actually.

    Glad you like the tutorial anyway :p
     
  7. Offline

    Icyene

    That is another interesting thing: all my other NMS entities had to be updated for 1.3.1 (loads of errors), but I saw meteors still worked, so I was like "well, this works, I don't know why, so better update the patch as well".
     
  8. I have a few suggestions:
    1. No need to call a(1.0F, 1.0F); as this is called by super(world);
    2.
    This:
    Code:java
    1. final Fireball fireball = (Fireball) this.getBukkitEntity(); fireball.getWorld().createExplosion(fireball.getLocation(), trailPower);

    gave me NPEs with a plugin listening for the EntityExplodeEvent, so I changed it to this:
    world.createExplosion(this, locX, locY, locZ, trailPower, true);
    3. I don't need your API as I can just get the Fireball (bukkit entity) and use it's API.

    So this is what I have till now:
    Code:java
    1. package de.V10lator.MayaApocalypse.Entity;
    2.  
    3. import net.minecraft.server.EntityFireball;
    4. import net.minecraft.server.MovingObjectPosition;
    5. import net.minecraft.server.World;
    6.  
    7. // Based on: [url]http://forums.bukkit.org/threads/tutorial-custom-entities-meteor.93899/[/url]
    8. public class MayaMeteor extends EntityFireball
    9. {
    10. private float speedMod = 1.1F;
    11. private float explosionRadius = 70F;
    12. private float trailPower = 10F;
    13. private float brightness = 10F;
    14.  
    15. public MayaMeteor(World world)
    16. {
    17. super(world);
    18. // No need to call a(1.0F, 1.0F); as super(world); still does this.
    19. }
    20.  
    21. @Override
    22. public void h_()
    23. {
    24. world.createExplosion(this, locX, locY, locZ, trailPower, true);
    25.  
    26. motX *= speedMod;
    27. motY *= speedMod;
    28. motZ *= speedMod;
    29.  
    30. super.h_();
    31. }
    32.  
    33. @Override
    34. public void a(MovingObjectPosition movingobjectposition)
    35. {
    36. world.createExplosion(this, locX, locY, locZ, explosionRadius, true);
    37. die();
    38. }
    39.  
    40. @Override
    41. public float c(float f)
    42. {
    43. return this.brightness;
    44. }
    45. }

    And this is how I spawn it:
    Code:java
    1. MayaMeteor mm = new MayaMeteor(nw);
    2. nw.addEntity(mm, SpawnReason.CUSTOM);
    3. Fireball meteor = (Fireball)mm.getBukkitEntity();
    4. meteor.teleport(loc);
    5.  
    6. meteor.setDirection(vec);
    7. meteor.setVelocity(vec);
    8. meteor.setBounce(false);
    9. meteor.setIsIncendiary(true);
    10. meteor.setYield(2 + rand.nextInt(14));

    Please not that this was quick&dirty and I'll improve it more future later. :)
     
  9. Offline

    Icyene

    I was wondering when someone would catch on to that :p. Correct, a(1.0....) is not needed. Yes, you can also get the Bukkit entity to use its API. However, specific things must be noted: some of my API was meteor-specific, such as setSpeedModifier() etc. Additionally, I would advise greatly against
    Code:
     meteor.setVelocity(vec);
    
    because when I used the same thing for meteor targetting, meteors will fail to spawn when you want them to. If you stay online for long enough, however, all meteors that haven't spawned yet will spawn at the same time, causing a massive lag spike that more often than not lags clients incredibly, even if damage isn't catastrophic.

    Additionally, and I'm just guessing here, but does world.createExplosion() fire an explosion event? If not, your meteor won't play nice with WorldGuard and other people's buildings. :p

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 27, 2016
  10. I think so (but I didn't check it... Will do that later).
    Well, that's not needed for me. In fact another plugin cancelling it wouldn't play nice with MayaApocalypse, but that's another story... ;)

    I checked it and the method does fire the events, too. :)

    But my meteors doesn't make the damage you showed in your screenshot. Well, that may have something to do with my method to stop the blocks from dropping their items.

    Icyene A bit off topic, but do you want to join the MayaApocalypse team (if beleg isn't against it) ? :)

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 27, 2016
  11. Offline

    Icyene

    1. Awesome!

    2. That is because that was under ideal circumstances, and kinda happened because of a bug I patched :D. To do it now, add an int called 'lives', and in a(), check if lives is 0. If so, make the big boom, and broadcast message. If not, make an explosion of decent size, and --lives. The explosion will allow the meteor to progress further, then impact again, burrow, and so on until lives = 0, at which point it will explode. To make it go to bedrock, use a lives like 10-20, and set the burrow explosion to around 15 (I had it at 10 for the image, but it doesn't account for the direction / velocity its coming down at (a lives of 20 at velocity.multiply(3) isnt that same as a lives of 20 @ normal velocity; same applies for pitch).

    Sure! Thanks :D I've wondered how you did the floods without lag :p
     
  12.  
  13. Offline

    Icyene

    With

    Code:
    else {
    world.createExplosion(this, locX, locY, locZ, burrowPower, true);
    }
    
     
  14. Icyene Isn't the else still handled by h_() ? BTW: PMed you. ;)
     
  15. Offline

    Icyene

    Right, forgot that you were using an oversized fireball explosion. And I replied :)
     
  16. Offline

    Icyene

    Update: To add onto the "efficiency" part. The meteor posted above will kill your FPS for quite a few seconds... This is because it is exploding 20x a second. Adding this as your h_ makes it only do it 2.5 times/second, as well as deletes itself if it goes under height 1, or higher than 255.

    Code:Java
    1.  
    2. @Override
    3. public void h_() {
    4. do {
    5. h_lock = !h_lock;
    6. if (h_lock) break;
    7. h_lock_2 = !h_lock_2;
    8. if (h_lock_2) break;
    9. h_lock_3 = !h_lock_3;
    10. if (h_lock_3) break;
    11.  
    12. int locY = (int)(this.locY);
    13. if ((locY & 0xFFFFFF00) != 0) { //locY < 1 or locY > 255
    14. this.dead = true; // Die silently
    15. return;
    16. }
    17.  
    18. if ((locY & 0xFFFFFFE0) == 0) { // locY < 32
    19. explode();
    20. return;
    21. }
    22.  
    23. world.createExplosion(this, locX, locY, locZ, trailPower, true);
    24. } while (false);
    25.  
    26. super.h_();
    27. }
    28.  


    All the _locks are booleans set to false at declaration. Note I am masking locY quite alot, I have commented to explain what each does.

    Enjoy! This improved the meteor's efficiency greatly, actually allowing me to move my camera around pretty well while the meteor was flying.
     
  17. Offline

    Comphenix

    Excellent tutorial. Not only is the subject material interesting, but it's also well explained and laid out. :)

    Just a small quibble - should you divide by 0.95 then? Like so:
    Code:
    motX /= 0.95F;
    motY /= 0.95F;
    motZ /= 0.95F;
    
    Otherwise the end result would be 0.95 * 1.10 = 1.0526, which results in 5% more velocity in each direction.
     
  18. If my math is correct 0.95 * 1.1 = 1.045 but anyway, wouldn't this be better:
    Code:java
    1. motX *= 0.91F;
    2. motY *= 0.91F;
    3. motZ *= 0.91F;

    Cause 0.91 * 1.1 = 1.001, so it still speeds up, but just by 0.1% - We could tell this is cause of gravity... :D
    or:
    Code:java
    1. motX *= 0.909F;
    2. motY *= 0.909F;
    3. motZ *= 0.909F;

    Then we have 0.909 * 1.1 = 0.999, so a little slowdown (0.1% again) - We could tell this is cause of the airs frictional resistance :D
     
  19. Offline

    Comphenix

    Oh wow, how could I miss that one? It's not like I had a long, detailed tutorial to write. :p

    In any case, the value I (incorrectly) gave is actually approximately equal to 1 / 0.95, the inverse of the original multiplication.

    Yeah, just like that. :p

    But if you read the tutorial, you'll notice that it's 0.95 we want to reverse, not 1.1:
    Code:
    motX *= 0.95F;
    motY *= 0.95F;
    motZ *= 0.95F;
    To reverse it:
    Code:
    motX *= 1.052632F;
    motY *= 1.052632F;
    motZ *= 1.052632F;
    Where 1.052632 * 0.95 ≈ 1. And this time I doubled checked the math. :p
     
    Icyene likes this.
  20. Offline

    AlphaCoder

    I can't find h_()! help
     
  21. AlphaCoder that's because this method is obfuscated and this obfuscation changes with every minecraft release. If you aren't experienced enough to handle this then don't use it.
     
  22. Offline

    krazytraynz

    Would you have to go about things this way for custom entities, or could you also just code like you would for a client mod and use the @Override annotation a lot?
     
  23. Offline

    bob7

    Having fun with this :)
     
    Icyene likes this.
  24. Offline

    TechTeller96

    I just want to know how to see which class corresponds to which .java file after decompilation, for example, I want to see the class file that corresponds to EntityMinecart.java
     
  25. Offline

    Icyene

    They get decompiled respectively... i.e. EntityMinecart.class will get decompiled to EntityMinecart.java. With the exception of class files containing nested classes, of course. Those are designated by 'A$B' where 'B' is the nested class. Even then, most modern decompilers merges 'A' and 'B' together.
     
  26. Offline

    TechTeller96

    No, but as you figured out that h_() corresponds to EntityFireball, how do I figure out which class file corresponds to EntityMinecart?
     
  27. Offline

    Icyene

    I am confused as to what you are asking... h_() is a method, not a class... h_() is j_() in the latest versions, and is part of the base Entity class.
     
  28. Offline

    TechTeller96

    Ohh, my bad - I meant how does one figure out which method corresponds to which in the decompiled version via MCP as opposed to the class.
     
  29. Offline

    Icyene

    TechTeller96

    Ah, the way you figure it out is completely up to you :)

    I tend to open both the CraftBukkit source and MCP code in windows side by side, and just find which method looks the most like which. Additionally, members are generally decompiled in the same order: so the first field in CB would be equivalent to the first field in MCP. This is not true for CB-added fields, but it holds for most.
     
  30. Offline

    chaseoes

    That screenshot is supposed to be Russia, right?
     
    TechTeller96 and Icyene like this.
Thread Status:
Not open for further replies.

Share This Page