Home > aen's Blog

Viewport Dragging

smee, build 12

Since everyone's gone kookoo for Google Maps, this build allows you to drag the viewport around in the same way. I ripped the hand cursors from Adobe Acrobat. Just hold down CTRL and drag with the mouse. I've also seen this in a very cool German map editor. Any way you slice it, it's pretty nifty for minor viewport movings.

I also added an Isometric Rules menu item to the new Options menu. This just enforces the restrictions on plotting when you're trying to build a map with isometric tiles (can only plot every other tile, offset by one every other row.)

Grid Size

I also created a version 2 map saver and loader which includes the grid size. Since tiles can be any size you want, the more relevant information becomes how they interlock, ie. the size of the grid on which they are aligned. I've included examples of both flavors in the archive.

Multiple Layers

The version 2 format also supports multiple layers. I've been upgrading a bunch of the guts in that direction. You can't create new layers yet, nor are they acknowledged during rendering, but the bulk of support for them is in there. Full support will be working its way into a build in the near future.

Mustang

I spent a lot of time fiddling around with Java 1.6 during the last few days, and I finally discovered the source of all my speed woes in relation to scrolling with JScrollPane. Apparently the implementation of JViewport, which the JScrollPane component makes use of, now has an implementation which causes a lot of unnecessary painting.

Luckily, I had already performed a bit of a rewrite of JViewport back when I first started fiddling around with 1.5, and it makes scrolling suitably speedy. It's named JViewportAlternative, and can be plugged into a scroll pane like so:

scroll.setViewport(new JViewportAlternative());

Easy peasy!

Even the 1.5 implementation of the viewport has a few issues, which caused me to look into the rewrite in the first place. Basically, it can scroll fast in one direction at a time. That is, left/right or up/down.

The refresh code in 1.5's implementation is such that it only allows for one rectangular area at a time to be shifted with the fast memory copy. That is, for scrolling the bulk of the unchanged viewport content in the appropriate direction. My rewrite consisted of copying the entire class, then tweaking it out until it correctly allowed diagonal movement with two memory copies.

It took quite a bit of work to figure out, but it was well worth it! It's a pretty damn useful implementation, and not one you'll readily find anywhere else. My initial motivation for creating it was for a minimap implementation, as in VERGE 3's maped3. It really shines for viewport dragging, since the scrolling increments are generally much smaller, but minimap viewport dragging benefits noticeably as well.

I was hoping 1.6 would improve the JViewport refresh implementation for diagonals... here's hoping the final release will! But if not, JViewportAlternative to the rescue!

Packages

There's a whole bunch of new packages which I separated many of the files out into.

smee.log includes a new Log class, which is an even simpler form of logging then Java's built-in logging API.

There are a couple minimal unit tests in smee.test, which I began adding after it became apparent that SMEE wasn't going to have a throwaway lifespan. I imagine I'll still be adding to it for at least a month to come. It's been a very good learning tool so far.

Most of the meat is in smee.ui — the alternative viewport implementation, the new map dialog, the map controller, renderer, tile selector, etc.

The smee.io package holds all of the map and tileset loading and saving classes, as well as a few exception classes.

Tile Selector

I updated the TileSelector class a bit. It occupes the full right hand side of the editor now.

The zoom slider looked kinda junky, so I just tossed it since I can't think of any reason to use it when mousewheeling is so much easier. Except for those without a mousewheel. But it's the new millennium for crying out loud. Who doesn't have a mouse wheel! I'll add it back at some point as soon as I decide on a better non-mousewheelian way to do it. Maybe a magnifying glass or something.

The tile selector has some interesting code inside get_tile_rect(). It operates on a simple enough idea. You proceed from left to right, displaying tiles as you go, keeping track of the tallest tile on the current row. When you hit the end of the row, skip down to the next and repeat.

This allows behavior similar to all the other map editor tile selectors you tend to see, that wrap the tiles within the available space of the tile selector container. It makes for a semi-wackified implementation though.

I didn't want to duplicate the code for mouse hit detection, so I refactored the algorithm into get_tile_rect(). It returns a rectangular region within the tile selector for a given tile based on its index. Using this method, I am able to loop through all tile indices and fit the corresponding tile images into these regions. Or, I can loop through them all and test whether the current mouse click position is within a region.

The tile selector allows only two zoom factors: 1 and 2. I decided on a standard width for the selector, which is 128 pixels. That allows for four 32x32 tiles across at a factor of 1 and 2 tiles across at a factor of 2. Doesn't look too shabby neither! I spent long minutes zooming back and forth, a large puddle of drool forming on my desk.

Scrolling Polished

Aside from the viewport dragging, I also updated the code to properly handle scrolling along the lower right edges with the keys when in tile scroll mode.

Originally, if the viewport was flush against the bottom, and then you scrolled to the right, the viewport would snap upward a little, depending on the size of the viewport.

A similar effect would occur if you were flush to the right, and scrolled downward. It was also disrupting movement using the scrollbars in the very bottom right corner. This is fixed now as well.

I also made the viewport dragging play nice with the tile scroll mode, so you can hit CTRL+T and enjoy the bumpy ride!

Loading & Saving

I spent a bit of time brushing up the loading and saving dialogs, and added a Save menu item to accompany Save As. If you try to save over an existing file, you are now warned. I also added the name of the current map to the title.

I refactored the loading and saving code away from the Map and Tileset classes. There was a sufficient amount of code for the loaders and savers to start clouding the implementation, plus it didn't seem correct that a map or tileset intrinsically would know about loading or saving.

This is an arguable point, certainly. It is much easier to perform loading "in house," but I like being able to throw internal data specific to the load or save operation into separate loader and saver classes without muddying up the map or tileset classes.

I also separated out loaders for specific versions into their own classes. Most of the time you will see loaders that implement all of the loading and saving code for different versions in the same method. I've written a lot of code like this myself. However, refactoring them into a class hierarchy allows me to utilize inheritance in a very sensical way.

I believe Tim Sweeney utilized inheritance in a similar fashoin for Unreal. New versions of the engine only ever added new code by inheriting from the old and overriding as necessary, so backward compatibility was never broken. There were caveats, which you've undoubtedly experienced if you've ever played Unreal Tournament with dissimilar versions, but overall it was a pretty impressive feat.

I leverage the inheritance mostly for common methods at this point, but the entire idea is one that I'll enjoy investigating further over time.

Tile Grid Implementation

I converted the tile grids from arrays of integers to arrays of Tile objects. I never liked the fact that when just using an integer array, there is no intrinsic way to represent "no tile." The grid is a lookup, and index 0 resolves to the tile at offset 0 in the tileset. You could of course alter the math to always subtract one from the offset when pulling the tile, but that requires additional shenanigans that feel less than creamy.

You could use a value like -1 to mean "no tile," but that is ultimately one of those dreaded "special values" that I mentioned in an earlier entry. With tile objects, I don't have to perform a lookup, and a null value lends itself easily to the concept of "no tile." There are tradeoffs, but I like how object references simplify my implementation.

It's also a good solution from the isometric map viewpoint, because as I mentioned in my last entry, the isometric look only works when tiles are limited to every other tile, stuttered by an offset of one every other row (like the "stipple alpha" effect used for maped2 obstructions.) With an integer grid, I would always have a tile at every single cell in the grid because 0 represents the first tile, and a new "empty" map is filled with zeroes by default.

Actions

When constructing the menus, there are a lot of temporary inner classes that extend ActionListener for the different actions the various menu items perform when clicked.

Eventually there'll be multiple places where I'll want to call the same code, so it behooves me to focus these operations into one location. To that end, I started creating action classes specific to each action: LoadMapAction and SaveMapAction.

These two classes extend AbstractAction, which implements the ActionListener interface. I create an object for each type of action and then they're ready to plug into menus and other places!

These will all get stuffed into the smee.action package.

Menus

I spent a little effort making the menus and menu items gray out appropriately as well. Right now that just means that Save, Save As..., and the View and Options menus are grayed out until you load or create a new map.

Plotting Quirks

Along the way I goofed up the fast refresh plotting somehow! Not sure what I did at the moment, since everything renders perfectly when the entire viewport is refreshed. I'll be palming my forehead some time in the near future I'm sure.

Until then, if you want 100% visual goodness when plotting tiles larger than the grid size, you can comment out everything in MapPanelRefreshManager's mapRefreshed() method and replace it with map_panel.repaint();

Helpers

I also created a smee.helper package for a few helper classes for cursors, exceptions, and images. I always seem to pile up quite a lot of utility methods that are fairly non-specfic, so this time I'm gonna make sure they all exist in more obvious locations!

Coming Up Next...

I've also been brushing up a few other little projects I have sitting about, to a point where they'll be more educational than confusing to poke through. I intended to put them out this weekend, but I think next weekend may be more timely.

In the meantime I'll be making further updates to SMEE. Four whole days without an update — it's a travesty I know! Having written more than 20 consecutive posts, sometimes more than one a day, I think I've come to the conclusion that it's good for an interlude every so often so you can spend some quality time focusing on research for bigger and better posts.

Most likely next I will be implementing multiple layers. What say you!