Resource [Tutorial] Maps

Discussion in 'Spigot Plugin Development' started by Stef, Apr 2, 2016.

  1. You might not know it, but maps are pretty useful, you can see where you are in a part of the world, scale those to cover a larger area on the map and create awesome looking world maps. This tutorial will cover some interesting things about maps with creating pixels on the map, scaling the map, adding text and images on the map, and add cursors to it. But let's start with a basic introduction to maps.

    Maps
    When getting a map, it will be empty by default. Right clicking it, will open the map on your point and load it (not entirely, you have to walk around to load more parts). By default a map has a scaling of 0. By putting paper around the map, will increase the size of the map to 1. This can be done until it reaches scale 4. Scale 0 covers 128 * 128 blocks (16,384 blocks). One pixel on the map represents one block. This number goes up to 2,048 * 2,048 blocks covering an area of 4,194,304 blocks (16,384 chunks). However one pixel on the map will now cover one entire chunk. Don't forget that a map always has 128 * 128 pixels, this is not changed by the scale of the map!

    Map initialization
    Let's get programming. In spigot there is a MapInitializeEvent, this will be called when someone opens a map for the first time. We're going to use this event to create our maps.
    Code (Text):
    @EventHandler
    public void onMapInitialize(MapInitializeEvent e) {

    }
    There is one method from the event where going to use today and that is the getMap() method. This returns a MapView. Make a variable so we can work with it in the next paragraph.
    Code (Text):
    MapView mapView = e.getMap();
    Scaling the map
    With the MapView we can scale our map, quite easily. The scale is set with the Scale enum. The scale isn't represented by a number, so here is a list to compare the Scale values with the numbers.
    0 - CLOSEST
    1 - CLOSE
    2 - NORMAL
    3 - FAR
    4 - FARTHEST
    The method for scaling is setScale(). Let's set our map view to farthest, just as an example.
    Code (Text):
    mapView.setScale(Scale.FARTHEST);
    That was just the easy part, let's move on to somewhat more difficult.

    Unlimited Tracking (1.11+ only)
    Unlimited tracking is a feature added in 1.11, but it's so tiny you probably wouldn't notice. Unlimited tracking means that even though you as a player are outside the range of the map, it'll still show your position on the side of the map which gets smaller if you're farther away from the range of the map. This technique is used with treasure maps and can be useful if you always want to show the relative position to the map of the player.
    Code (Text):
    mapView.setUnlimitedTracking(true);
    There's also a getter for this:
    Code (Text):
    boolean unlimitedTracking = mapView.getUnlimitedTracking();
    Rendering and setting pixels
    Let's create our own renderer. Renderers are used to determine what will be shown on the map. But first we have to clear minecraft default renderers. Keep in mind when you do not cover the entire area with an image, text or pixels miencraft will still generate landscape behind it when walking. Therefore it's wise to use up the entire map. Clearing the renderers is not that hard. We can get all the renderers with getRenderers() and then clear them.
    Code (Text):
    mapView.getRenderers().clear();
    Now we want to add our own renderer. We can do this with addRenderer(). This takes in a class that extends from MapRenderer. Let's just put new Renderer() as parameter for now and than we create our class. That's convenient for this tutorial, because we don't have to jump back and forth between classes.
    Code (Text):
    mapView.addRenderer(new Renderer());
    In case the map is contextual (different for each player) specify true as the first parameter inside the creation of the renderer (1.11+ only):
    Code (Text):
    mapView.addRenderer(new Renderer(true));
    Now create a new class called Renderer (or how you named it in your addRenderer method). Let this class extend from the MapRenderer abstract class.
    Code (Text):
    public class Renderer extends MapRenderer {

    }
    It'll ask you to add a method, the render method. Just let your IDE import this for you. If you're using notepad++ or something else to program (not recommended :) ), here is the method.
    Code (Text):
    @Override
    public void render(MapView mapView, MapCanvas mapCanvas, Player player) {

    }
    We can use the mapCanvas to draw all kind of different stuff on the screen. Let start simple, with the setPixel. With this method we can set a pixel on a specific x and y (x is width, y is height) with a color. There are 13 different colors and a transparanet color (that's a total of 14). We can find these in the MapPalette class. Almost this whole class is deprecated, but that doesn't matter because it still works. Let's set a red pixel at x = 56 and y = 72.
    Code (Text):
    mapCanvas.setPixel(56, 72, MapPalette.RED);
    Hint: A common way to create large areas of pixels are nested for loops. One is for the x and one for the y.
    Code (Text):
    for (int x = 25; x < 50; x++) {
      for (int y = 25; y < 50; y++) {
        mapCanvas.setPixel(x, y, MapPalette.RED);
      }
    }
    This will set every pixel from x = 25 to x = 49 and from y = 25 to y = 49 to a red color.

    Drawing text
    Within the mapCanvas we can also draw text with the drawText method. We can specify an x and y to start writing, a font and of course the text. Let's draw the player's name on the map. First we need an x and y to start from. Let's start from x = 15 and y = 15. Than we need to specify a font. This sounds cool, but it isn't because there is only one font. This is the MinecraftFont.Font. Now we only need to specify our text and that will be the player's name, so player.getName() (player is from the render method and is the player who is holding the map). When we combine all these things together we get this.
    Code (Text):
    mapCanvas.drawText(15, 15, MinecraftFont.Font, player.getName());
    Good job, we got ourself a nice map now. But what if we want images, you're not going to set 16,384 pixels yourself do you? Luckily we can use the drawImage method.

    Drawing images
    Drawing images is one of the most useful functions for maps, but it has some flaws which you have to keep in mind. The rendering process of a image is very slow, this can take up five seconds and will lag your server. It is recommended to do this on a different thread. Also if you're always going to draw the same image you should cache the image, because your render method will be called every 20 milliseconds (on average). With that images from the internet can be copyrighted or you don't have permission to use the image (that's called a 403 error). We can catch the 403 error, but when using an image from someone else, please ask the author permission to use his/her image. With that being said, let's continue programming.

    Drawing images which you get from the internet might sound harder than it actually is. But first we need to make a try/catch statement with an IOExcpetion. This has two reasons. The first one; it's required, because the class which will load the images throws this exception. Second; when we have a 403 error (explained above), we can catch it here. When this isn't caught it will spam your entire server with errors, until this map is deleted. The try/catch statement.
    Code (Text):
    try {

    } catch (IOException e) {

    }
    In the exception you probably want to clear the player's inventory, so it doesn't spam the server and give them a warning that the image couldn't be loaded (most likely because of a 403 error).
    Code (Text):
    player.getInventory().clear();
    player.sendMessage(ChatColor.RED + "The image couldn't be loaded. This is likely due to a 403 error.");
    return;
     
    Let's start drawing this image. First we need an x and y where we start drawing the image. Because I'm going to use a image that takes up the entire map I'll leave it at 0, 0. Now we need to load the image. We're going to use the ImageIO class. The ImageIO class is a class that loads images from files, urls and inputstreams. You can also use it to get some readers and writers for your image, but we're not going to use that today. We'll be focusing on the URL one. To read an image we can simply call read() on ImageIO, because this method is static. Than we can specify an URL. An URL is a class which can retrieve information from URLs. We use the simple version today, the url from a string. Create a new instance of URL and pass the url as a string. Don't worry if you didn't follow this completely this is how it'll look like.
    Code (Text):
    BufferedImage image = ImageIO.read(new URL("http://myawesomewebsite.com/image.png")); //should be initialized outside the render method
    mapCanvas.drawImage(0, 0, image);
    We are done with the most common features of maps, but there is one last thing I want to show you; map cursors.

    Creating map cursors
    There are a bunch of ways to create your own map cursor and almost all of them are deprecated, but don't worry they all work fine.

    First, cursors are small icons on a map, normally to represent where you are. But of course we can modify this to make them appear where we want.

    First we need to create a new MapCursorCollection. This is basically a list with cursors. We need this to later set all cursors. We can create our collection by creating a new instance of it.
    Code (Text):
    MapCursorCollection cursors = new MapCursorCollection();
    Now we can add cursors with the addCursor method. We start with the easiest one, the one which takes an int, an int and a byte. The ints are for the x and y. The byte is the direction it's facing. Because there is no documentation on which byte equals which direction, I tested it for you. This is a list of all the directions:
    0 = South
    1 = South-West-South
    2 = South-West
    3 = South-West-West
    4 = West
    5 = North-West-West
    6 = North-West
    7 = North-West-North
    8 = North
    9 = North-East-North
    10 = North-East
    11 = North-East-East
    12 = East
    13 = South-East-East
    14 = South-East
    15 = South-East-South
    Let's create a cursor that points to east and is located at 60, 70.
    Code (Text):
    cursors.addCursor(60, 70, (byte) 12);
    We can also create a cursor which contains an int, an int, a byte and a byte. This is the same as the last one, but the last byte is the type of the pointer. There is no documentation for this either so I tested those for you.
    0 = White pointer
    1 = Green pointer
    2 = Red pointer
    3 = Blue pointer
    4 = White clover
    5 = Red bold pointer
    6 = White dot
    7 = Light blue square
    Let's create the same cursor, but with as a white clover.
    Code (Text):
    cursors.addCursor(60, 70, (byte) 12, (byte) 4);
    1.11+:
    There now is a MapCursor.Type class which you can use to get these values in order to minimalize the use of magic values. There are also three new types: Mansion, Temple and White_Circle. The constructor of the MapCursor still uses a byte though, so you'll need to convert it to one by calling getValue().

    We have another method. This one uses an int, an int, a byte, a byte and a boolean. This is the same as last time, but the boolean is for the visibility of the cursor. To demonstrate this let's create the same cursor again, but specifically specify that it is visible.
    Code (Text):
    cursors.addCursor(60, 70, (byte) 12, (byte) 4, true);
    Now there is only one method left. This one uses the MapCursor class, which is completely deprecated. The mapCursor constructor takes the same parameters as the last one and does exactly the same, but the two ints are now changed into two bytes. Here is an example.
    Code (Text):
    cursors.addCursor(new MapCursor((byte) 60, (byte) 70, (byte) 12, (byte) 4, true));
    We now covered every method of the cursors, but we have to add it to our MapCanvas. We can do this by calling setCursors(). This takes in the MapCursorCollection we created and draws them on the map.
    Code (Text):
    mapCanvas.setCursors(cursors);
    Good job, you made it to the end of the tutorial. If you have any questions or requests for any future tutorials leave them in the reactions.
     
    #1 Stef, Apr 2, 2016
    Last edited: Mar 8, 2017
    • Useful x 23
    • Like x 12
    • Winner x 6
    • Informative x 2
    • Agree x 1
  2. Good tutorial, I like it.

    Only one thing, doesn't this get called very often from the main Spigot thread? Not sure about this. And if it is not the main Spigot thread, why do it so often anyway? Shouldn't this be done just once?
    Code (Text):
    mapCanvas.drawImage(0, 0, ImageIO.read(new URL("http://myawesomewebsite.com/image.png")));
     
    • Like Like x 1
  3. By far the best tutorial I have ever seen about maps. Thanks for this awesomeness. Definitely going to use this in my next plugin. :D
     
    • Agree Agree x 1
  4. The MapInitializeEvent only gets called when a map is rendered for the first time. So using images shouldn't have a drastic impact on your server's performance. However if you have multiple players which will see the same image I suggest saving the image in a variable and retrieving it from there.

    EDIT: The rendering gets called multiple times. I suggest making a boolean stating if the image has to still be loaded or not.
    Thank you.
     
    #4 Stef, May 6, 2016
    Last edited: May 7, 2016
  5. nice tutorial! thanks
     
  6. You're welcome.
     
  7. Bookmarked! Good job =D
     
    • Agree Agree x 1
  8. Thank you :)
     
    • Useful Useful x 1
  9. If I want to keep updating the map cursors, would I have to keep removing and adding a renderer? Or could would I use p#sendMap()?
     
  10. You don't need to remove and add anything. If you want to add something add a certain point, get your renderer from your MapView and call a method that adds this to the renderer. An example:
    Code (Text):
    public class Renderer extends MapRenderer {
     
      private List<String> textsToDraw = new ArrayList<String>();

      @Override
      public void render(MapView view, MapCanvas canvas, Player player) {
        for (String text : textsToDraw) {
          canvas.drawText(x, y, MinecraftFont.Font, text);
        }
        textsToDraw.clear();
      }

      public void addText(String text) {
        textsToDraw.add(text);
      }
    }
    Now you can get your renderer from the MapView and call addText on it. The next time it renders it will add your text on the map.
     
  11. But I want to update the map while it is in the players hand. What would I do for that?
     
  12. All renderers which are saved in the map are called continuously, so it should automatically render after a few milliseconds. Using Player#sendMap(MapView map) only speeds upthe process of rendering the map if I'm right.
     
  13. So the render() method gets called every few milliseconds?
    That seems a bit resource intensive.
     
  14. That's how it works and how games work in general. A game has a loop which never ends, in there update and render methods get called. The update method updates everything like entity positions, collision, blocks etc. The render method draws everything to your screen. To know what should be rendered on the map it calls all renderers inside the mapview and renders what's in it. This whole process is done within a few milliseconds. You can calculate how long it takes to do all this stuff by doing 1 / fps = amount of seconds it takes to do this. So if I have 60 fps it takes roughly 0,02 seconds or 20 milliseconds. This is also the reason why you should clear the inventory when an error occurs (as seen in the paragraph Drawing images) if you get an error it will spam your server because you renderer will be called over and over and only stops when it doesn't have to render it anymore.
     
    • Informative Informative x 1
  15. Ahh, okay thank you very much! I'll let you know if I have any other questions.
     
    • Like Like x 1
  16. You're welcome.
     
  17. In need, will help me a lot in a project. :p
     
  18. Thanks for this, but is there a way to fully render out the map?
     
  19. This is a really nice tutorial for anyone new to maps.

    I was actually working on a maps plugin a while ago which honestly made it incredibly easy for even beginner developers to get very advanced with maps.
    I still have the project, but I kind of abandoned it, only due to just not having much time to work anymore.

    Basically if you wanted something on the map, you would simply create a class and extend RenderItem and that would give you a method with the parameters Graphics, SMap (The custom map object), and Player.
    Because you were given a Graphics object, you have a lot more tools than just setting individual pixels.
    Of course if you were doing more simple things, you could just use one of the premade render items: Background, Image, Solid, Text, and Selector.
    Background - Fills the entire map with a color, draws on layer -1 by default.
    Image - Inserts an image onto the map from a file or URL. Can be positioned and scaled (proportionally or not)
    Solid - Creates a box with x, y, width, height, and Color paramters.
    Text - The Text item is real nice because it allows different font sizes, and color codes or rgb colors within the map's palette (which was a pain to code btw)
    Selector - Allows to create a list of options for the player to scroll through and select.
    And it was easy to create actions for the map since you could just override the methods onRightClick or onLeftClick and do whatever you like when a player clicks the map. No need to create your own listeners and test to see if they are the correct map.
     
  20. I need help, you can make a little tutorial for destroy a block when a player click on that. I star a month's ago to code with spigot, but i don't understan. (sorry for my english)
     

Share This Page