Your choice of music can be everything when it comes to your video project. We've put together some of our favourite pieces, loosely categorised to make it easy for you to find what you want.
A beautiful and inspiring melodic piano piece, which kicks into rousing orchestra – perfect for films, presentations or YouTube videos that are ready to tug on the heartstrings.
A motivational orchestral piece featuring strings, horns and percussion. A calm introduction builds, calms, builds and finally calms again into a soft piano finish.
With a longer and shorter version included in the download, the Uplifting and Inspiring Emotional Adventure Trailer piece will have you scaling the greatest of heights. Metaphorically of course.
Timeless City is a track that shares characteristics from different genres like downtempo, chill step, uplifting trance and chill out, all combined in a unique way.
Absorbed by Space begins with piercing, clear sounds, full of mystery. Sounds in this piece are varied, but the tones are rounded and polished in a way that resembles fluidity.
A usefully looping track, Hip-Hop Background Beat is a light hip-hop track with a deliberately young, urban sound.
Coda
If you're as emotionally wrung out as we are after listening to so many pieces of wonderful music, then take a few minutes to look at these other articles to help you to nail your film project's music.
Background score is the music you are using in a film. It's so important, and choosing the right music, when to add it, how to edit it are all very important...
In video production, the process of selecting music for projects can be tricky. While there are plenty of great tracks available on sites like AudioJungle...
What is best in games? To crush your enemies, see them driven before you, and get cool weapon upgrades? Sadly, the alternative answer—"to heal the sick, bring people together, and make everyone happy"—never really took off.
This is the heart of most games. As designers, we spend hours upon hours designing unique weapons and devious traps, but when it comes to healing, we mostly just say, "Oh, and we'll throw in some health packs." Healing isn't cool.
But why does it have to be like this? Why is it that no one ever wants to be the healer? Is it even possible to make healing fun? Lets look a bit more into how healing works, and examine why things are like this.
What Is Health?
First, we need to understand the purpose health serves in a game.
The most fundamental purpose of health is to provide a win/lose system for the player. In almost any game with combat, be it roleplaying games, beat-em-ups, or even card games, the goal is simple: make their health hit zero first.
Generally speaking, health is an all-or-nothing attribute—it has no impact on the game until you run out. Some games do try to make health a bit more nuanced by implementing "pain", where your crosshair wobbles a bit or you limp, but generally, these are minor inconveniences at best.
But because health is something the player can control to an extent, it's also a resource. Players will often make decisions based on the amount of health they have left, and will often be willing to "trade" health for other benefits.
At a basic level, this includes things like rocket jumping or running through heavy fire to secure an objective, but also includes more complex concepts like using low health to bait enemies into an ambush.
You might ask, "What about health in other games, like Surgeon Simulator or Trauma Centre?" Yes, there is a sort of health system, but these games are puzzle games, and the health concept is really just a thematic way of giving the player a timer. Conversely, there are games which use "health in disguise", such as shields, hunger, or fatigue. As a general rule, if you die when it hits zero, it's health.
So Why Do We Heal?
Why is it important to heal? Simply, because healing allows us to progress through the game. A traditional RPG might have the players encounter several groups of goblins before a big boss fight. If the player wasn't allowed to heal, then minor wounds sustained during the adventure might make the boss fight impossible.
Healing allows the players to treat each fight as a separate combat, and means that the final boss fight can feel like a properly epic clash, rather than forcing the players to use cheap tactics because everyone arrived on 1 hitpoint. Most combat games follow the same idea, allowing players a brief respite and a chance to heal between skirmishes.
For most fighting games, healing is less important. In a game like Street Fighter or Mortal Kombat, the player only has one fight. In a campaign/story mode, the player starts each fight on full health, negating the need for healing (although some characters might have special moves which restore a minor amount of health). We don't really need a healing mechanic because our health is automatically reset each fight.
The resetting of health is a massively important concept to keeping the player engaged in the game. We've talked before about frustrating the player, and how important it is to keep the player engaged. Imagine playing a game where you finish a large combat, only to stroll into the next room and be instantly killed by the weakest enemy in the game.
Worse, imagine finishing that fight and knowing that the next room will kill you. What do you do? In order to progress the game, your only option is to walk forward into certain death? Locking a player into an unwinnable situation is massively frustrating.
Then Why Is Healing Bad?
The problem we have is that when a player is healing, they're not really taking part in the game. Wandering around looking for healthpacks isn't exciting, and if there are no healthpacks available then the player might find themselves stuck in an unwinnable situation. This is one of the reasons the original Halo did so well, being arguably the first mainstream FPS to utilise a regenerating health.
The existence of healthpacks also pose another problem: sudden health spikes. Because health bars act as a sort of "goal", players can use enemy health as a progress bar. It can be frustrating for you to reduce an enemy to 3 hitpoints, only to have them run over a healthpack and be restored to full health. Healing potions are often guilty of this as well, as players can stock up before battle, ensuring victory is given to the player who simply brought the most potions to the fight.
The solution to all this is reasonably simple: allow players to heal as much as they want, once combat has finished. There are a few ways do go about this:
Regenerating health (or shields), used in games like Halo.
Food and drink, as used in games like World of Warcraft, which is really just a slightly fancier out-of-combat regeneration health option. Players must take a few moments to sit down and replenish their health and mana reserves. The benefit with food and drink is that it feels slightly more "sensible" than health magically regenerating, and forcing the player to sit down while they heal can be considered a good way to set a calm, resting feeling.
Return to base: popular with MOBAs like League of Legends, where returning to your home fountain will very rapidly heal you up.
Potions which slowly regenerate health, rather than ones which cause health spikes.
Healing points: special map points which heal players inside. These healing stations can be put as objectives for players to fight over, or simply deployed as "forward bases" for wounded players to retreat to.
So What About the Medic?
The existence of healers provides a whole new range of problems for most games. The healer is one of the "holy trinity" of class design (tank-damage-healer), so most games with some sort of role selection have at least one healer.
The biggest issue is that if a game has healer, then self-healing players render the healer redundant. Some games have decided that only the healer can restore health—which then means that the healer is, ultimately, the only class which is 100% mandatory.
Sadly, the healer is also frequently the least interesting class to play, meaning that the healer role becomes something that players need to play (in order to win), rather than something they want to play. In Team Fortress 2, the medic is by far and away the least popular class, as can be seen in this graph.
This isn't unique to Team Fortress 2, however, as pretty much any game which features a "support" role will struggle to find players who want to play that role. There are several reasons for this:
Support classes tend to have less of a visible impact on the game, which feels unrewarding.
They tend not to be the ones to score kills and objectives (which, once again, feels less rewarding).
Games tend to verbally/visually reward players for killstreaks and multi-kills, but very rarely for a "heal streak" or "well-timed shield"—once again, less rewarding.
Playing a support class is effectively sacrificing personal power for team power, which rewards those playing non-support classes.
Playing a healer often means following other players around, effectively putting the player on a leash, removing their agency.
And, for many online games, being a healer means being blamed by other players.
For the most part, there is an obvious theme here. A large part of the problem comes from the fact that while a player is supporting, they're not really playing the game. A healer needs to spend all their time healing, not hitting objectives and shooting bombs. The choices a player makes are minimal, and they don't get the same level of satisfaction from doing well. For most players, being the healer means sacrificing fun to win.
Keeping Healers Involved
Most of these problems come from traditional views of how a healer should be designed. There's nothing necessarily wrong with the idea of a healer class, but the "pure healer" approach to design is often flawed. As we mentioned earlier, in many games, having a healer in the team is necessary for success. Because of this, healers are often kept out of danger—and out of the action.
One option for this is to remove the healer class and allow players to deal with healing themselves. If everyone has access to regenerating health, for example, then the use of a healer becomes more of a choice. Healers can still exist, but their skillset can be geared towards combat healing and allowing them to participate in the action.
It's also possible to make combat-capable healers—or, at least, make healers that don't have to focus exclusively on healing. Here are some examples of games that tried to make healing a little more interesting:
In the now defunct Warhammer Online, there were six different healer classes, with a variety of abilities. Most notably, the human and dark elf healers gained healing power for being in melee combat, making them effective front-line healers and rewarding them for getting into combat. Similarly, goblin shamans had a sort of yin/yang design for damage and healing, which meant that casting a spell of one type would give a boost to the other. Effective play meant that the player had to cast a mix of healing and damage spells.
In Paladins, classes are given specific roles, but all roles are expected to be engaged in combat. The healer classes are all capable combatants, and healing is often a "fire and forget" affair.
As an example, Grohk can simply drop a totem which has an ongoing AOE heal effect, and Pip can throw a "healing bomb" which heals everyone within a short area. Grover, a tanky frontline healer, has a passive healing aura—everyone near him will slowly regenerate health, meaning his presence is all that's required for a healing boost.
Finally, in Dystopia, players can choose one of three classes: light, medium, or heavy. Each class can be outfitted with a variety of abilities (implants), such as stealth, leg boosters, or thermal vision.
The light and medium classes can be equipped with a mediplant, which passively uses energy to heal nearby wounded allies. This ability can be turned off to save energy, but the fact that the healing is automatic means the healer is free to concentrate on combat rather than wounded teammates.
All three of these games utilise a similar approach to the healer—namely, that the healer should be engaged in gameplay, and not hiding in the back lines babysitting allies who find themselves caught out of position. Players are still expected to participate in objective pushes and combat, and players who do attempt to sit back and heal people will not be a great asset to the team.
Making Healing Fun
But what if we want to make a traditional healing class? There's nothing wrong with that, but it's important to understand why healing roles traditionally fail.
The biggest issue is the idea of choice. As we've talked about in previous articles, choice is one of the most fundamental aspects of game design. Since the role of the healer is largely limited to "follow me and keep healing", the options they have tend to be very limited. In order to fix this, we simply need to give the player more options.
World of Warcraft manages this to a degree: healing classes have a range of spells, and balancing these spells is essential to maximising combat effectiveness. These spells will generally include:
a general purpose healing spell
a mana-intensive fast heal for emergencies
a slow but efficient heal-over-time spell
an area-of-effect heal for groups
a "one-use" extra powerful heal with a significant cooldown
By giving the healer a variety of heals, it allows them a lot more variation than simply clicking the same "heal" button over and over. Healing requires a balance of using heal spells effectively as well as making sure they don't draw aggro from the tank.
An alternative to this is giving the player a mixture of defensive spells, and letting them figure out which is the best. While this might not be suitable for all games, it's an option which can be considered. Imagine, for example, the following spells:
Heal: Heals for 10% of health.
Rewind: Heal all damage from last attack.
Shield: Provides a health bonus equal to 20% for five seconds.
Resist: Halves all damage for three seconds.
Vampirism: Buff which gives health back for attacking enemies.
A variety of spells means the player needs to be able to react to specific situations better, rather than simply hitting the heal button over and over. There are a multitude of ways to make spells more interesting than simply "heal someone", and these are just some basic ideas.
On top of this, if players are able to self-heal, then a healer doesn't even need to heal—they can throw buffs and debuffs around, protecting allies with shields and slowing enemies with curses. The core gameplay element is the same, but the player now has different options.
Rewarding the Healer
This is a small point, but worth bringing up again. Because game objectives traditionally revolve around scoring kills and securing objectives, healing can often feel unrewarding. To make matters worse, some games reward players that score kills with points, experience, or gold, whereas the healer might only get a fraction of that reward.
Make sure your healers feel as if they're contributing. If a healer does well, reward them. If your game has a scoreboard, make sure your healers and supports earn points for doing their job. If your only metric for success is having a positive kill:death ratio, then of course the healers are going to feel undervalued.
If your healer throws up that shield at the right time, or gives you a speed boost so you can catch the wounded enemy, then they deserve the kill as much as you do. Statistics like lives saved, wards placed,
Additionally, ensure that the healer feels rewarding to play. Many players don't notice healing—they only notice when they die—so the effect of healing can go unnoticed. Psychologically, players tend to favour big, splashy effects over subtle ones, even if the subtle effect is better. Compare a skill which continually heals all teammates nearby for a small amount, vs. a skill which provides an invulnerability shield for a few seconds.
Even though constant healing is much better in any sort of extended fight, the invulnerability shield allows for clutch moments where a player is saved with a sliver of health. Team Fortress 2 allows medics to heal with the ubercharge, a powerful shield which provides eight seconds of invulnerability to the medic and a friend. It can be argued that the medics' true power lies in the ubercharge, which can be used to break stalemates or push important objectives.
As a word of warning, however, be aware that powerful healers can create very stagnant gameplay, which is why many games shy away from the idea. This was common in the early days of World of Warcraft, where druids generally had low damage, but high defence and healing power—meaning a duel between two druids would literally last forever, as they could simply outheal everything thrown at them.
Its not unreasonable for a healer to desire the same sort of game-changing power that a mage or warrior might have, but if the healer is able to "lock the game down", then it's unlikely to be much fun for the opposing side.
Putting It All Together
At the end of the day, all of this depends on the type of game you're trying to make. Regenerating health isn't some sort of instant fix that automatically makes games good—in fact, sometimes it's important to not let the players heal. In an RPG setting, the players might have to go through several fights without being allowed to heal.
This can increase tension, allow pacing to be maintained, and force the players to either retreat or approach combat in a more tactical way than "kill everything that moves".
Similarly, restricting access to healing can provide additional challenge. In a fighting game, the player might find be good at fighting 1v1, but what if they weren't allowed to heal between fights? Suddenly, combat changes—the player needs to be more aware of their environment, and combat becomes more of a risk. This concept is often used in survival games, where running away from enemies is as important as engaging.
It's also worth noting that many of these ideas are simple to apply to other concepts in game design as well. Ammo, mana, and even gold coins are limited resources. If the player finds themselves stuck in an unwinnable situation, then you need to look at giving them ways to get back into the game.
Ultimately, your game is your game, and there is no right and wrong way to do healing. What matters is making the game fun and keeping your players entertained. Like many other aspects of game design, these concepts are to be used or discarded at your leisure.
If your gameplay revolves around pressing the same button over and over, then it's not likely to be very engaging. But if you can give your players a variety of choices and keep them on their toes, then it's much more likely that they'll have a good time.
SpriteKit is Apple's 2D game engine—a rendering engine built on top of OpenGL. It was introduced with iOS 7, and each subsequent release has brought great additions to the framework. With the
use of textured sprites, a built-in physics engine, and the very
powerful SKAction class, you can very quickly build functional 2D games.
SpriteKit has built-in editors for scenes and particles, a camera node since the release of iOS9, and built-in support for tilesets since the release of iOS 10. With these new additions, SpriteKit is quickly becoming a powerhouse for creating 2D games.
Below is an image of the built-in scene editor, with a label, a colored sprite, and a textured sprite.
Rendering Loop
SpriteKit, like most game engines, uses a rendering loop to render and update the screen. The rendering loop goes through the following steps in rendering each scene:
Update the scene and its objects
Evaluate actions
Simulate physics
Apply constraints
Render the scene
Each of these steps has a corresponding method you can hook into to apply additional logic. The render loop methods are as follows:
update
didEvaluateActions
didSimulatePhysics
didApplyConstraints
didFinishUpdate
For example, if you wanted to manually move objects in your scene, then the update method would be what you would use. Or if you had objects that were being affected by actions or physics, you could tie into the corresponding methods to make sure those actions and the physics simulation are applied before whatever changes you make.
SKNode
The SKNode class is the fundamental building block of SpriteKit. All of your onscreen assets will be an SKNode or subclass thereof.
The SKNode class does not draw any visual assets itself. Its primary role is to provide baseline behavior that other classes implement. For example, the SKScene class is the root node in a tree of SKNode instances and is used to hold sprites and other content that needs to be rendered.
The rendering and animation are done by an SKView instance. The view is placed inside a window and an SKScene instance is added to it, and that scene will be rendered and animated as long as the view is active. You can use a single SKView instance in your window and switch between different scenes at any time.
The framework defines a number of other SKNode subclasses. The most common one used within a scene is the SKSpriteNode class. The SKSpriteNode class can be drawn either as a rectangle with an image mapped onto it with SKTexture, to create a sprite, or as a colored, untextured rectangle. You'll most often
use textured sprites, because this is how you will bring your game's artwork
to life.
Other important types of nodes include:
SKShapeNode, which renders a shape defined by a Core Graphics path
SKVideo, which displays video content
SKLabel, which displays a text label
We'll look at several of these subclasses of SKNode later in this series.
SKAction
The SKAction class is a very powerful class that is used to bring your nodes to life. SKAction can change your node's properties over time, for example by moving, scaling, or rotating them. You can chain actions together in a sequence, execute many actions together as a group, and repeat them in a loop. You can also use SKAction to run a custom block of code. For example, suppose you wanted to print out the coordinates of a node after it has moved. You could run a custom block of code within the SKAction to do just that.
SpriteKit Features
Physics
SpriteKit has a built-in physics engine that makes handling complex physics scenarios a breeze. Built on top of the popular Box2D framework, it allows you to respond to collisions and contact events, apply forces and gravity, and build very complex physics simulations using joints, such as pins and springs. You can use the scene editor to visually add physics to the nodes, or you can add physics programmatically.
Coordinate System
In SpriteKit, the coordinate (0,0) is
located at the bottom left of the screen instead of the top left, which
you may be used to if you've worked with Flash, Corona, HTML5 Canvas,
and many other game frameworks. Having the origin at the bottom left is an OpenGL convention, and SpriteKit follows it because SpriteKit uses OpenGL
under the hood.
Particle System
SpriteKit has a very powerful particle engine which can be used to simulate particle systems such as fire and smoke. There is also a built-in particle editor where you can visually lay out particle systems. If you prefer to stick with code, you can program these systems from the ground up using nothing but code.
Below is an image of the particle editor with a fire-like particle system.
Tiles
SpriteKit has a number of classes dedicated to building tiled layouts. Using tilemaps offers better memory usage than using a very large single image. The tiles can be arranged in rectangular, hexagonal, or isometric grids.
Below is an image of a tile map node using a rectangular grid.
Conclusion
These are a few of the highlights of the SpriteKit engine. I would suggest reading the SpriteKit overview to learn more about what it has to offer. To learn more about how to get started with SpriteKit, you should also check out Davis Allie's post here on Envato Tuts+.
Also, check out our SpriteKit courses! These will take you through all the steps of building your first SpriteKit game for iOS, even if you've never coded with SpriteKit before.
We have all played our fair share of amazing isometric games, be it the original Diablo, or Age of Empires or Commandos. The first time you came across an isometric game, you may have wondered if it was a 2D game or a 3D game or something completely different. The world of isometric games has its mystical attraction for game developers as well. Let us try to unravel the mystery of isometric projection and try to create a simple isometric world in this tutorial.
This tutorial is an updated version of my existing tutorial on creating isometric worlds. The original tutorial used Flash with ActionScript and is still relevant for Flash or OpenFL developers. In this new tutorial I have decided to use Phaser with JS code, thereby creating interactive HTML5 output instead of SWF output.
Please be advised that this is not a Phaser development tutorial, but we are just using Phaser to easily communicate the core concepts of creating an isometric scene. Besides, there are much better and easier ways to create isometric content in Phaser, such as the Phaser Isometric Plugin.
For the sake of simplicity, we will use the tile-based approach to create our isometric scene.
1. Tile-Based Games
In 2D games using the tile-based approach, each visual element is broken down into smaller pieces, called tiles, of a standard size. These tiles will be arranged to form the game world according to pre-determined level data—usually a two-dimensional array.
Usually tile-based games use either a top-down view or a side view for the game scene. Let us consider a standard top-down 2D view with two tiles—a grass tile and a wall tile—as shown here:
Both of these tiles are square images of the same size, hence the tile height and tile width are the same. Let us consider a game level which is a grassland enclosed on all sides by walls. In such a case, the level data represented with a two-dimensional array will look like this:
Here, 0 denotes a grass tile and 1 denotes a wall tile. Arranging the tiles according to the level data will produce our walled grassland as shown in the image below:
We can go a bit further by adding corner tiles and separate vertical and horizontal wall tiles, requiring five additional tiles, which leads us to our updated level data:
Check out the image below, where I have marked the tiles with their corresponding tile numbers in the level data:
Now that we have understood the concept of the tile-based approach, let me show you how we can use a straightforward 2D grid pseudo code to render our level:
for (i, loop through rows)
for (j, loop through columns)
x = j * tile width
y = i * tile height
tileType = levelData[i][j]
placetile(tileType, x, y)
If we use the above tile images then the tile width and tile height are equal (and the same for all tiles), and will match the tile images' dimensions. So the tile width and tile height for this example are both 50 px, which makes up the total level size of 300 x 300 px—that is, six rows and six columns of tiles measuring 50 x 50 px each.
As discussed earlier, in a normal tile-based approach, we either implement a top-down view or a side view; for an isometric view, we need to implement the isometric projection.
2. Isometric Projection
The best technical explanation of what isometric projection means, as far as I'm aware, is from this article by Clint Bellanger:
We angle our camera along two axes (swing the camera 45 degrees to one side, then 30 degrees down). This creates a diamond (rhombus) shaped grid where the grid spaces are twice as wide as they are tall. This style was popularized by strategy games and action RPGs. If we look at a cube in this view, three sides are visible (top and two facing sides).
Although it sounds a bit complicated, actually implementing this view is very easy. What we need to understand is the relation between 2D space and the isometric space—that is, the relation between the level data and the view; the transformation from top-down Cartesian coordinates to isometric coordinates. The image below shows the visual transformation:
Placing Isometric Tiles
Let me try to simplify the relationship between level data stored as a 2D array and the isometric view—that is, how we transform Cartesian coordinates into isometric coordinates. We will try to create the isometric view for our now-famous walled grassland. The 2D view implementation of the level was a straightforward iteration with two loops, placing square tiles offsetting each with the fixed tile height and tile width values. For the isometric view, the pseudo code remains the same, but the placeTile() function changes.
The original function just draws the tile images at the provided coordinates x and y, but for an isometric view we need to calculate the corresponding isometric coordinates. The equations to do this are as follows, where isoX and isoY represent isometric x- and y-coordinates, and cartX and cartY represent Cartesian x- and y-coordinates:
Yes, that is it. These simple equations are the magic behind isometric projection. Here are Phaser helper functions which can be used to convert from one system to another using the very convenient Point class:
function cartesianToIsometric(cartPt){
var tempPt=new Phaser.Point();
tempPt.x=cartPt.x-cartPt.y;
tempPt.y=(cartPt.x+cartPt.y)/2;
return (tempPt);
}
function isometricToCartesian(isoPt){
var tempPt=new Phaser.Point();
tempPt.x=(2*isoPt.y+isoPt.x)/2;
tempPt.y=(2*isoPt.y-isoPt.x)/2;
return (tempPt);
}
So we can use the cartesianToIsometric helper method to convert the incoming 2D coordinates into isometric coordinates inside the placeTile method. Apart from this, the rendering code remains the same, but we need to have new images for the tiles. We cannot use the old square tiles used for our top-down rendering. The image below shows the new isometric grass and wall tiles along with the rendered isometric level:
Unbelievable, isn't it? Let's see how a typical 2D position gets converted to an isometric position:
2D point = [100, 100];
// isometric point will be calculated as below
isoX = 100 - 100; // = 0
isoY = (100 + 100) / 2; // = 100
Iso point == [0, 100];
Similarly, an input of [0, 0] will result in [0, 0], and [10, 5] will give [5, 7.5].
For our walled grassland, we can determine a walkable area by checking whether the array element is 0 at that coordinate, thereby indicating grass. For this we need to determine the array coordinates. We can find the tile's coordinates in the level data from its Cartesian coordinates using this function:
function getTileCoordinates(cartPt, tileHeight){
var tempPt=new Phaser.Point();
tempPt.x=Math.floor(cartPt.x/tileHeight);
tempPt.y=Math.floor(cartPt.y/tileHeight);
return(tempPt);
}
(Here, we essentially assume that tile height and tile width are equal, as in most cases.)
Hence, from a pair of screen (isometric) coordinates, we can find tile coordinates by calling:
This screen point could be, say, a mouse click position or a pick-up position.
Registration Points
In Flash, we could set arbitrary points for a graphic as its centre point or [0,0]. The Phaser equivalent is Pivot. When you place the graphic at say [10,20], then this Pivot point will get aligned with [10,20]. By default, the top left corner of a graphic is considered its [0,0] or Pivot. If you try to create the above level using the code provided, then you will not get the displayed result. Instead, you will get a flat land without the walls, like below:
This is because the tile images are of different sizes and we are not addressing the height attribute of the wall tile. The below image shows the different tile images that we use with their bounding boxes and a white circle where their default [0,0] is:
See how the hero gets misaligned when drawing using the default pivots. Also notice how we lose the height of the wall tile if drawn using default pivots. The image on the right shows how they need to be properly aligned so that the wall tile gets its height and the hero gets placed in the middle of the grass tile. This issue can be solved in different ways.
Make all tiles in the same image size with the graphic aligned properly within the image. This creates a lot of empty areas within each tile graphic.
Set pivot points manually for each tile so that they align properly.
Draw tiles with specific offsets so that they align properly.
For this tutorial, I have chosen to use the third method so that this works even with a framework without the ability to set pivot points.
3. Moving in Isometric Coordinates
We will never try to move our character or projectile in isometric coordinates directly. Instead, we will manipulate our game world data in Cartesian coordinates and just use the above functions for updating those on the screen. For example, if you want to move a character forward in the positive y-direction, you can simply increment its y property in 2D coordinates and then convert the resulting position to isometric coordinates:
y = y + speed;
placetile(cartesianToIsometric(new Phaser.Point(x, y)))
This will be a good time to review all the new concepts that we have learned so far and to try and create a working example of something moving in an isometric world. You can find the necessary image assets in the assets folder of the source git repository.
Depth Sorting
If you tried to move the ball image in our walled garden then you would come across the problems with depth sorting. In addition to normal placement, we will need to take care of depth sorting for drawing the isometric world, if there are moving elements. Proper depth sorting makes sure that items closer to the screen are drawn on top of items farther away.
The simplest depth sorting method is simply to use the Cartesian y-coordinate value, as mentioned in this Quick Tip: the further up the screen the object is, the earlier it should be drawn. This may work well for very simple isometric scenes, but a better way will be to redraw the isometric scene once a movement happens, according to the tile's array coordinates. Let me explain this concept in detail with our pseudo code for level drawing:
for (i, loop through rows)
for (j, loop through columns)
x = j * tile width
y = i * tile height
tileType = levelData[i][j]
placetile(tileType, x, y)
Imagine our item or character is on the tile [1,1]—that is, the topmost green tile in the isometric view. In order to properly draw the level, the character needs to be drawn after drawing the corner wall tile, both the left and right wall tiles, and the ground tile, like below:
If we follow our draw loop as per the pseudo code above, we will draw the middle corner wall first, and then will continue to draw all the walls in the top right section until it reaches the right corner.
Then, in the next loop, it will draw the wall on the left of the character, and then the grass tile on which the character is standing. Once we determine this is the tile which occupies our character, we will draw the character after drawing the grass tile. This way, if there were walls on the three free grass tiles connected to the one on which the character is standing, those walls will overlap the character, resulting in proper depth sorted rendering.
4. Creating the Art
Isometric art can be pixel art, but it doesn't have to be. When dealing with isometric pixel art, RhysD's guide tells you almost everything you need to know. Some theory can be found on Wikipedia as well.
When creating isometric art, the general rules are:
Start with a blank isometric grid and adhere to pixel-perfect precision.
Try to break art into single isometric tile images.
Try to make sure that each tile is either walkable or non-walkable. It will be complicated if we need to accommodate a single tile that contains both walkable and non-walkable areas.
Most tiles will need to seamlessly tile in one or more directions.
Shadows can be tricky to implement, unless we use a layered approach where we draw shadows on the ground layer and then draw the hero (or trees, or other objects) on the top layer. If the approach you use is not multi-layered, make sure shadows fall to the front so that they won't fall on, say, the hero when he stands behind a tree.
In case you need to use a tile image larger than the standard isometric tile size, try to use a dimension which is a multiple of the iso tile size. It is better to have a layered approach in such cases, where we can split the art into different pieces based on its height. For example, a tree can be split into three pieces: the root, the trunk, and the foliage. This makes it easier to sort depths as we can draw pieces in corresponding layers which correspond with their heights.
Isometric tiles that are larger than the single tile dimensions will create issues with depth sorting. Some of the issues are discussed in these links:
First we will need to fix how many directions of motion are permitted in our game—usually, games will provide four-way movement or eight-way movement. Check out the image below to understand the correlation between the 2D space and isometric space:
Please note that a character would be moving vertically up when we press the up arrow key in a top-down game, but for an isometric game the character will move at a 45-degree angle towards the top right corner.
For a top-down view, we could create one set of character animations facing in one direction, and simply rotate them for all the others. For isometric character art, we need to re-render each animation in each of the permitted directions—so for eight-way motion, we need to create eight animations for each action.
For ease of understanding, we usually denote the directions as North, North-West, West, South-West, South, South-East, East, and North-East. The character frames below show idle frames starting from South-East and going clockwise:
We will place characters in the same way that we placed tiles. The movement of a character is accomplished by calculating the movement in Cartesian coordinates and then converting to isometric coordinates. Let us assume we are using the keyboard to control the character.
We will set two variables, dX and dY, based on the directional keys pressed. By default, these variables will be 0 and will be updated as per the chart below, where U, D, R, and L denote the Up, Down, Right, and Left arrow keys, respectively. A value of 1 under a key represents that the key is being pressed; 0 implies that the key is not being pressed.
So dX and dY stand for the change in the x- and y-positions of the character, based on the keys pressed. We can easily calculate the new isometric coordinates, as we've already discussed:
Iso = cartesianToIsometric(new Phaser.Point(newX, newY))
Once we have the new isometric position, we need to move the character to this position. Based on the values we have for dX and dY, we can decide which direction the character is facing and use the corresponding character art. Once the character is moved, please don't forget to repaint the level with the proper depth sorting as the tile coordinates of the character may have changed.
Collision Detection
Collision detection is done by checking whether the tile at the newly calculated position is a non-walkable tile. So, once we find the new position, we don't immediately move the character there, but first check to see what tile occupies that space.
In the function isWalkable(), we check whether the level data array value at the given coordinate is a walkable tile or not. We must take care to update the direction in which the character is facing—even if he does not move, as in the case of him hitting a non-walkable tile.
Now this may sound like a proper solution, but it will only work for items without volume. This is because we are only considering a single point, which is the midpoint of the character, to calculate collision. What we really need to do is to find all the four corners of the character from its available 2D midpoint coordinate and calculate collisions for all of those. If any corner is falling inside a non-walkable tile, then we should not move the character.
Depth Sorting With Characters
Consider a character and a tree tile in the isometric world, and they both have the same image sizes, however unrealistic that sounds.
To properly understand depth sorting, we must understand that whenever the character's x- and y-coordinates are less than those of the tree, the tree overlaps the character. Whenever the character's x- and y-coordinates are greater than that of the tree, the character overlaps the tree. When they have the same x-coordinate, then we decide based on the y-coordinate alone: whichever has the higher y-coordinate overlaps the other. When they have the same y-coordinate then we decide based on the x-coordinate alone: whichever has the higher x-coordinate overlaps the other.
As explained earlier, a simplified version of this is to just sequentially draw the levels starting from the farthest tile—that is, tile[0][0]—and then draw all the tiles in each row one by one. If a character occupies a tile, we draw the ground tile first and then render the character tile. This will work fine, because the character cannot occupy a wall tile.
6. Demo Time!
This is a demo in Phaser. Click to focus on the interactive area and use your arrow keys to move the character. You may use two arrow keys to move in the diagonal directions.
You can find the complete source for the demo in the source repository for this tutorial.
In this final part of the tutorial series, we'll build on the first tutorial and learn about implementing pickups, triggers, level swapping, path finding, path following, level scrolling, isometric height, and isometric projectiles.
1. Pickups
Pickups are items that can be collected within the level, normally by simply walking over them—for example, coins, gems, cash, ammo, etc.
Pickup data can be accommodated right into our level data as below:
In this level data, we use 8 to denote a pickup on a grass tile (1 and 0 represent walls and walkable tiles respectively, as before). This could be a single tile image with a grass tile overlaid with the pickup image. Going by this logic, we will need two different tile states for every tile which has a pickup, i.e. one with pickup and one without to be shown after the pickup gets collected.
Typical isometric art will have multiple walkable tiles—suppose we have 30. The above approach means that if we have N pickups, we will need N x 30 tiles in addition to the 30 original tiles, as each tile will need to have one version with pickups and one without. This is not very efficient; instead, we should try to dynamically create these combinations.
To solve this, we could use the same method we used to place the hero in the first tutorial. Whenever we come across a pickup tile, we will place a grass tile first and then place the pickup on top of the grass tile. This way, we just need N pickup tiles in addition to 30 walkable tiles, but we would need number values to represent each combination in the level data. To solve the need for N x 30 representation values, we can keep a separate pickupArray to exclusively store the pickup data apart from the levelData. The completed level with the pickup is shown below:
For our example, I am keeping things simple and not using an additional array for pickups.
Picking Up Pickups
Detecting pickups is done in the same way as detecting collision tiles, but after moving the character.
if(onPickupTile()){
pickupItem();
}
function onPickupTile(){//check if there is a pickup on hero tile
return (levelData[heroMapTile.y][heroMapTile.x]==8);
}
In the function onPickupTile(), we check whether the levelData array value at the heroMapTile coordinate is a pickup tile or not. The number in the levelData array at that tile coordinate denotes the type of pickup. We check for collisions before moving the character but need to check for pickups afterwards, because in the case of collisions the character should not occupy the spot if it is already occupied by the collision tile, but in case of pickups the character is free to move over it.
Another thing to note is that the collision data usually never changes, but the pickup data changes whenever we pick up an item. (This usually just involves changing the value in the levelData array from, say, 8 to 0.)
This leads to a problem: what happens when we need to restart the level, and thus reset all pickups back to their original positions? We do not have the information to do this, as the levelData array has been changed as the player picked up items. The solution is to use a duplicate array for the level while in play and to keep the original levelData array intact. For instance, we use levelData and levelDataLive[], clone the latter from the former at the start of the level, and only change levelDataLive[] during play.
For the example, I am spawning a random pickup on a vacant grass tile after each pickup and incrementing the pickupCount. The pickupItem function looks like this.
function pickupItem(){
pickupCount++;
levelData[heroMapTile.y][heroMapTile.x]=0;
//spawn next pickup
spawnNewPickup();
}
You should notice that we check for pickups whenever the character is on that tile. This can happen multiple times within a second (we check only when the user moves, but we may go round and round within a tile), but the above logic won't fail; since we set the levelData array data to 0 the first time we detect a pickup, all subsequent onPickupTile() checks will return false for that tile. Check out the interactive example below:
2. Trigger Tiles
As the name suggests, trigger tiles cause something to happen when the player steps on them or presses a key when on them. They might teleport the player to a different location, open a gate, or spawn an enemy, to give a few examples. In a sense, pickups are just a special form of trigger tiles: when the player steps on a tile containing a coin, the coin disappears and their coin counter increases.
Let's look at how we could implement a door that takes the player to a different level. The tile next to the door will be a trigger tile; when the player presses the x key, they'll proceed to the next level.
To change levels, all we need to do is swap the current levelData array with that of the new level, and set the new heroMapTile position and direction for the hero character. Suppose there are two levels with doors to allow passing between them. Since the ground tile next to the door will be the trigger tile in both levels, we can use this as the new position for the character when they appear in the level.
The implementation logic here is the same as for pickups, and again we use the levelData array to store trigger values. For our example, 2 denotes a door tile, and the value beside it is the trigger. I have used 101 and 102 with the basic convention that any tile with a value greater than 100 is a trigger tile and the value minus 100 can be the level which it leads to:
var level1Data=
[[1,1,1,1,1,1],
[1,1,0,0,0,1],
[1,0,0,0,0,1],
[2,102,0,0,0,1],
[1,0,0,0,1,1],
[1,1,1,1,1,1]];
var level2Data=
[[1,1,1,1,1,1],
[1,0,0,0,0,1],
[1,0,8,0,0,1],
[1,0,0,0,101,2],
[1,0,1,0,0,1],
[1,1,1,1,1,1]];
The code for checking for a trigger event is shown below:
var xKey=game.input.keyboard.addKey(Phaser.Keyboard.X);
xKey.onUp.add(triggerListener);// add a Signal listener for up event
function triggerListener(){
var trigger=levelData[heroMapTile.y][heroMapTile.x];
if(trigger>100){//valid trigger tile
trigger-=100;
if(trigger==1){//switch to level 1
levelData=level1Data;
}else {//switch to level 2
levelData=level2Data;
}
for (var i = 0; i < levelData.length; i++)
{
for (var j = 0; j < levelData[0].length; j++)
{
trigger=levelData[i][j];
if(trigger>100){//find the new trigger tile and place hero there
heroMapTile.y=j;
heroMapTile.x=i;
heroMapPos=new Phaser.Point(heroMapTile.y * tileWidth, heroMapTile.x * tileWidth);
heroMapPos.x+=(tileWidth/2);
heroMapPos.y+=(tileWidth/2);
}
}
}
}
}
The function triggerListener() checks whether the trigger data array value at the given coordinate is greater than 100. If so, we find which level we need to switch to by subtracting 100 from the tile value. The function finds the trigger tile in the new levelData, which will be the spawn position for our hero. I have made the trigger to be activated when x is released; if we just listen for the key being pressed then we end up in a loop where we swap between levels as long as the key is held down, since the character always spawns in the new level on top of a trigger tile.
Here is a working demo. Try picking up items by walking over them and swapping levels by standing next to doors and hitting x.
3. Projectiles
A projectile is something that moves in a particular direction with a particular speed, like a bullet, a magic spell, a ball, etc. Everything about the projectile is the same as the hero character, apart from the height: rather than rolling along the ground, projectiles often float above it at a certain height. A bullet will travel above the waist level of the character, and even a ball may need to bounce around.
One interesting thing to note is that isometric height is the same as height in a 2D side view, although smaller in value. There are no complicated conversions involved. If a ball is 10 pixels above the ground in Cartesian coordinates, it could be 10 or 6 pixels above the ground in isometric coordinates. (In our case, the relevant axis is the y-axis.)
Let's try to implement a ball bouncing in our walled grassland. As a touch of realism, we'll add a shadow for the ball. All we need to do is to add the bounce height value to the isometric Y value of our ball. The jump height value will change from frame to frame depending on the gravity, and once the ball hits the ground we'll flip the current velocity along the y-axis.
Before we tackle bouncing in an isometric system, we'll see how we can implement it in a 2D Cartesian system. Let's represent the jump power of the ball with a variable zValue. Imagine that, to begin with, the ball has a jump power of 100, so zValue = 100.
We'll use two more variables: incrementValue, which starts at 0, and gravity, which has a value of -1. Each frame, we subtract incrementValue from zValue, and subtract gravity from incrementValue in order to create a dampening effect. When zValue reaches 0, it means the ball has reached the ground; at this point, we flip the sign of incrementValue by multiplying it by -1, turning it into a positive number. This means that the ball will move upwards from the next frame, thus bouncing.
The code remains the same for the isometric view as well, with the slight difference that you can use a lower value for zValue to start with. See below how the zValue is added to the isometric y value of the ball while rendering.
function drawBallIso(){
var isoPt= new Phaser.Point();//It is not advisable to create points in update loop
var ballCornerPt=new Phaser.Point(ballMapPos.x-ball2DVolume.x/2,ballMapPos.y-ball2DVolume.y/2);
isoPt=cartesianToIsometric(ballCornerPt);//find new isometric position for hero from 2D map position
gameScene.renderXY(ballShadowSprite,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture
gameScene.renderXY(ballSprite,isoPt.x+borderOffset.x+ballOffset.x, isoPt.y+borderOffset.y-ballOffset.y-zValue, false);//draw hero to render texture
}
Check out the interactive example below:
Do understand that the role played by the shadow is a very important one which adds to the realism of this illusion. Also, note that we're now using the two screen coordinates (x and y) to represent three dimensions in isometric coordinates—the y-axis in screen coordinates is also the z-axis in isometric coordinates. This can be confusing!
4. Finding and Following a Path
Path finding and path following are fairly complicated processes. There are various approaches using different algorithms for finding the path between two points, but as our levelData is a 2D array, things are easier than they might otherwise be. We have well-defined and unique nodes which the player can occupy, and we can easily check whether they are walkable.
A detailed overview of pathfinding algorithms is outside of the scope of this article, but I will try to explain the most common way it works: the shortest path algorithm, of which A* and Dijkstra's algorithms are famous implementations.
We aim to find nodes connecting a starting node and an ending node. From the starting node, we visit all eight neighbouring nodes and mark them all as visited; this core process is repeated for each newly visited node, recursively.
Each thread tracks the nodes visited. When jumping to neighbouring nodes, nodes that have already been visited are skipped (the recursion stops); otherwise, the process continues until we reach the ending node, where the recursion ends and the full path followed is returned as a node array. Sometimes the end node is never reached, in which case the path finding fails. We usually end up finding multiple paths between the two nodes, in which case we take the one with the smallest number of nodes.
Path Finding
It is unwise to reinvent the wheel when it comes to well-defined algorithms, so we would use existing solutions for our path-finding purposes. To use Phaser, we need a JavaScript solution, and the one I have chosen is EasyStarJS. We initialise the path-finding engine as below.
easystar = new EasyStar.js();
easystar.setGrid(levelData);
easystar.setAcceptableTiles([0]);
easystar.enableDiagonals();// we want path to have diagonals
easystar.disableCornerCutting();// no diagonal path when walking at wall corners
As our levelData has only 0 and 1, we can directly pass it in as the node array. We set the value of 0 as the walkable node. We enable diagonal walking capability but disable this when walking close to the corners of non-walkable tiles.
This is because, if enabled, the hero may cut into the non-walkable tile while doing a diagonal walk. In such a case, our collision detection will not allow the hero to pass through. Also, please be advised that in the example I have completely removed the collision detection as that is no longer necessary for an AI-based walk example.
We will detect the tap on any free tile inside the level and calculate the path using the findPath function. The callback method plotAndMove receives the node array of the resulting path. We mark the minimap with the newly found path.
game.input.activePointer.leftButton.onUp.add(findPath)
function findPath(){
if(isFindingPath || isWalking)return;
var pos=game.input.activePointer.position;
var isoPt= new Phaser.Point(pos.x-borderOffset.x,pos.y-borderOffset.y);
tapPos=isometricToCartesian(isoPt);
tapPos.x-=tileWidth/2;//adjustment to find the right tile for error due to rounding off
tapPos.y+=tileWidth/2;
tapPos=getTileCoordinates(tapPos,tileWidth);
if(tapPos.x>-1&&tapPos.y>-1&&tapPos.x<7&&tapPos.y<7){//tapped within grid
if(levelData[tapPos.y][tapPos.x]!=1){//not wall tile
isFindingPath=true;
//let the algorithm do the magic
easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove);
easystar.calculate();
}
}
}
function plotAndMove(newPath){
destination=heroMapTile;
path=newPath;
isFindingPath=false;
repaintMinimap();
if (path === null) {
console.log("No Path was found.");
}else{
path.push(tapPos);
path.reverse();
path.pop();
for (var i = 0; i < path.length; i++)
{
var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x);
tmpSpr.tint=0x0000ff;
//console.log("p "+path[i].x+":"+path[i].y);
}
}
}
Path Following
Once we have the path as a node array, we need to make the character follow it.
Say we want to make the character walk to a tile that we click on. We first need to look for a path between the node that the character currently occupies and the node where we clicked. If a successful path is found, then we need to move the character to the first node in the node array by setting it as the destination. Once we get to the destination node, we check whether there are any more nodes in the node array and, if so, set the next node as the destination—and so on until we reach the final node.
We will also change the direction of the player based on the current node and the new destination node each time we reach a node. Between nodes, we just walk in the required direction until we reach the destination node. This is a very simple AI, and in the example this is done in the method aiWalk shown partially below.
function aiWalk(){
if(path.length==0){//path has ended
if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){
dX=0;
dY=0;
isWalking=false;
return;
}
}
isWalking=true;
if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){//reached current destination, set new, change direction
//wait till we are few steps into the tile before we turn
stepsTaken++;
if(stepsTaken<stepsTillTurn){
return;
}
console.log("at "+heroMapTile.x+" ; "+heroMapTile.y);
//centralise the hero on the tile
heroMapSprite.x=(heroMapTile.x * tileWidth)+(tileWidth/2)-(heroMapSprite.width/2);
heroMapSprite.y=(heroMapTile.y * tileWidth)+(tileWidth/2)-(heroMapSprite.height/2);
heroMapPos.x=heroMapSprite.x+heroMapSprite.width/2;
heroMapPos.y=heroMapSprite.y+heroMapSprite.height/2;
stepsTaken=0;
destination=path.pop();//whats next tile in path
if(heroMapTile.x<destination.x){
dX = 1;
}else if(heroMapTile.x>destination.x){
dX = -1;
}else {
dX=0;
}
if(heroMapTile.y<destination.y){
dY = 1;
}else if(heroMapTile.y>destination.y){
dY = -1;
}else {
dY=0;
}
if(heroMapTile.x==destination.x){
dX=0;
}else if(heroMapTile.y==destination.y){
dY=0;
}
//......
}
}
We do need to filter out valid click points by determining whether we've clicked within the walkable area, rather than a wall tile or other non-walkable tile.
Another interesting point for coding the AI: we do not want the character to turn to face the next tile in the node array as soon as he has arrived in the current one, as such an immediate turn results in our character walking on the borders of tiles. Instead, we should wait until the character is a few steps inside the tile before we look for the next destination. It is also better to manually place the hero in the middle of the current tile just before we turn, to make it all feel perfect.
Check out the working demo below:
5. Isometric Scrolling
When the level area is much larger than the available screen area, we will need to make it scroll.
The visible screen area can be considered as a smaller rectangle within the larger rectangle of the complete level area. Scrolling is, essentially, just moving the inner rectangle inside the larger one. Usually, when such scrolling happens, the position of the hero remains the same with respect to the screen rectangle, commonly at the screen center. Interestingly, all we need to implement scrolling is to track the corner point of the inner rectangle.
This corner point, which we represent in Cartesian coordinates, will fall within a tile in the level data. For scrolling, we increment the x- and y-position of the corner point in Cartesian coordinates. Now we can convert this point to isometric coordinates and use it to draw the screen.
The newly converted values, in isometric space, need to be the corner of our screen too, which means they are the new (0, 0). So, while parsing and drawing the level data, we subtract this value from the isometric position of each tile, and can determine if the tile's new position falls within the screen.
Alternatively, we can decide we are going to draw only an X x Y isometric tile grid on screen to make the drawing loop efficient for larger levels.
We can express this in steps as so:
Update Cartesian corner point's x- and y-coordinates.
Convert this to isometric space.
Subtract this value from the isometric draw position of each tile.
Draw only a limited predefined number of tiles on screen starting from this new corner.
Optional: Draw the tile only if the new isometric draw position falls within the screen.
var cornerMapPos=new Phaser.Point(0,0);
var cornerMapTile=new Phaser.Point(0,0);
var visibleTiles=new Phaser.Point(6,6);
//...
function update(){
//...
if (isWalkable())
{
heroMapPos.x += heroSpeed * dX;
heroMapPos.y += heroSpeed * dY;
//move the corner in opposite direction
cornerMapPos.x -= heroSpeed * dX;
cornerMapPos.y -= heroSpeed * dY;
cornerMapTile=getTileCoordinates(cornerMapPos,tileWidth);
//get the new hero map tile
heroMapTile=getTileCoordinates(heroMapPos,tileWidth);
//depthsort & draw new scene
renderScene();
}
}
function renderScene(){
gameScene.clear();//clear the previous frame then draw again
var tileType=0;
//let us limit the loops within visible area
var startTileX=Math.max(0,0-cornerMapTile.x);
var startTileY=Math.max(0,0-cornerMapTile.y);
var endTileX=Math.min(levelData[0].length,startTileX+visibleTiles.x);
var endTileY=Math.min(levelData.length,startTileY+visibleTiles.y);
startTileX=Math.max(0,endTileX-visibleTiles.x);
startTileY=Math.max(0,endTileY-visibleTiles.y);
//check for border condition
for (var i = startTileY; i < endTileY; i++)
{
for (var j = startTileX; j < endTileX; j++)
{
tileType=levelData[i][j];
drawTileIso(tileType,i,j);
if(i==heroMapTile.y&&j==heroMapTile.x){
drawHeroIso();
}
}
}
}
function drawHeroIso(){
var isoPt= new Phaser.Point();//It is not advisable to create points in update loop
var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y);
isoPt=cartesianToIsometric(heroCornerPt);//find new isometric position for hero from 2D map position
gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);//draw shadow to render texture
gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);//draw hero to render texture
}
function drawTileIso(tileType,i,j){//place isometric level tiles
var isoPt= new Phaser.Point();//It is not advisable to create point in update loop
var cartPt=new Phaser.Point();//This is here for better code readability.
cartPt.x=j*tileWidth+cornerMapPos.x;
cartPt.y=i*tileWidth+cornerMapPos.y;
isoPt=cartesianToIsometric(cartPt);
//we could further optimise by not drawing if tile is outside screen.
if(tileType==1){
gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false);
}else{
gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false);
}
}
Please note that the corner point is incremented in the opposite direction to the hero's position update as he moves. This makes sure that the hero stays where he is with respect to the screen. Check out this example (use arrows to scroll, tap to increase the visible grid).
A couple of notes:
While scrolling, we may need to draw additional tiles at the screen borders, or else we may see tiles disappearing and appearing at the screen extremes.
If you have tiles that take up more than one space, then you will need to draw more tiles at the borders. For example, if the largest tile in the whole set measures X by Y, then you will need to draw X more tiles to the left and right and Y more tiles to the top and bottom. This makes sure that the corners of the bigger tile will still be visible when scrolling in or out of the screen.
We still need to make sure that we don't have blank areas on the screen while we are drawing near the borders of the level.
The level should only scroll until the most extreme tile gets drawn at the corresponding screen extreme—after this, the character should continue moving in screen space without the level scrolling. For this, we will need to track all four corners of the inner screen rectangle, and throttle the scrolling and player movement logic accordingly. Are you up for the challenge to try implementing that for yourself?
Conclusion
This series is particularly aimed at beginners trying to explore isometric game worlds. Many of the concepts explained have alternate approaches which are a bit more complicated, and I have deliberately chosen the easiest ones.
They may not fulfil most of the scenarios you may encounter, but the knowledge gained can be used to build upon these concepts to create more complicated solutions. For example, the simple depth sorting implemented will break when we have multi-storied levels and platform tiles moving from one story to the other.
Hello, Augmented Reality traveller! In this post, I'll show you some cool features of Vuforia that make it possible to create engaging AR apps. We'll take a look at Cube and Cylinder Targets, Smart Terrain, VuMarks, and more. We won't dive deep into any of those subjects, but I'll try to cover enough to get you started.
If you aren't familiar with Vuforia's main concepts, check out some of my earlier posts. They'll get you started building your first app with Vuforia, right from scratch.
With the viral success of Pokemon GO, everybody's been talking about augmented reality. In this tutorial we'll look at using Vuforia for augmented reality....
In this tutorial we’ll start to build an app with Augmented Reality using Vuforia on Unity 3D. We’ll learn how to set up Vuforia and start developing an AR...
With the viral success of Pokemon GO, everybody's been talking about augmented reality. In this tutorial we'll finish creating an AR game that can be easily...
In this series, we've been building an app with Augmented Reality using Vuforia on Unity 3D. Now, we'll improve our game with Vuforia's Image Target resource.
1. Setting Up Vuforia on Unity
This section is just a review for those who have used Vuforia in the past. If you're already familiar with the process of preparing a Unity project for Vuforia, feel free to skip this section.
Before using any of Vuforia’s
resources on Unity, you’ll have to get the framework set up. First of all, you’ll
have to download
and import the Vuforia package for Unity.
Then you’ll need
to create a License Key for the project in Vuforia's License Manager. Once it's created, you’ll take the
key and insert it on an ARCamera prefab, and that’s it. Now you can start playing around with Vuforia.
In this tutorial we’ll start to build an app with Augmented Reality using Vuforia on Unity 3D. We’ll learn how to set up Vuforia and start developing an AR...
2. Designing an ImageTarget
There are multiple ways to create Augmented Reality experiences on Vuforia, and almost all of them rely on some kind of Target that must be recognized by Vuforia's algorithm to start the augmentation process. You can design a target yourself and submit it to Vuforia's Target Manager, making it recognizable by the system. However, this design must conform to some guidelines, otherwise the target won't be tracked easily, or it might not even be recognized at all.
You especially need to know how to design an ImageTarget. That's because a lot of other kinds of Vuforia targets are composed of multiple ImageTargets arranged in some specific position and order with a MultiTarget.
There are three main rules that an ImageTarget must comply with:
It must be rich in detail.
It must have a good contrast, with bright and dark regions.
No repetitive patterns can be present.
In the background, Vuforia creates an arrangement of the image using itsfeatures, and then the algorithm can find such patterns and track the targets. Roughly, a feature in an image is a sharpened angle, like a box corner or the tip of a star. The quantity of features in an image is directly connected to its 'trackability'.
However, it's important to understand that even hundreds of features in an image won't help if those features are arranged in a pattern. The ImageTargets need some grade of randomness and chaos to be properly recognized.
If you keep those rules in mind, you'll be able to create excellent ImageTargets, but if you need to know a little more, read the documentation.
3. Using MultiTarget
Also known as Cuboid Target, the MultiTarget consists of a series
of ImageTargets in a defined geometric arrangement. This arrangement allows Vuforia's algorithm to track the targets at the same time, creating a volumetric reference. MultiTarget can be very useful in marketing campaigns to promote consumer interaction with product packages and so on.
3.1 Designing a MultiTarget
Basically, a MultiTarget design must conform to the same rules as an ImageTarget, adding two other concerns: the Depth of the box and its Geometric consistency.
The box's Depth should be at least half of its width. This is only a recommendation, and it will work if it's a little smaller than that, but it's interesting to keep that in mind.
Also, Vuforia's algorithm expects consistency on all parts of the MultiTarget. That means that all of the sides of the box are expected to be in place. If this isn't possible, it would be possible to keep the removable part blank when creating the target in Target Manager. A cereal box lid, for example, could be left blank, otherwise once the lid was open it could generate inconsistencies during the augmentation.
3.2 Creating a MultiTarget
The first step is to add or select a database in Vuforia’s Target Manager. Select the database and click on Add Target,
selecting the Cuboid option and setting its Width, Height, Length, andName.
After the MultiTarget has been created, select it and set its ImageTargets. As I mentioned, a MultiTarget is composed of a series of ImageTargets adjusted in a specific position. Each part of the cuboid must contain an image, and each image should
comply with the cuboid’s proportion, defined when the target was
created.
When the Cuboid is set, you’ll be ready to go. Just
download the database and import it to Unity. To use it, drag a
MultiTarget prefab to the stage, and select the downloaded database
and Cuboid Target. To learn more about MultiTarget, take a look at the documentation.
4. Cylinder Targets
According to the Vuforia documentation, “CylinderTargets enable you to detect and track images rolled into cylindrical
and conical shapes.” You
can use this kind of interaction to create engagement with product
packages, like soda cans or any other cylindrical product.
4.1 Designing a Cylinder Target
A CylinderTarget is also based on the ImageTarget, hence its design should conform to those rules. The top and bottom of the cylinder must be square images that will be marked by the Target Manager system. As with MultiTarget, you should consider the target's consistency, meaning that if a part of the object can be removed, it would be helpful to leave it blank on the manager.
4.2 Creating
a Cylinder Target
This kind of target is created in two steps. The
first one is toAdd
or Select a database in Vuforia’s Target Manager, add a new Target, selecting the Cylinder option, and
set its Dimensions and Name.
Next, you’ll need to select the CylinderTarget you created and upload images to it. You can upload one image for the side, one for the bottom, and one for the top. Just click on the desired section and upload the image.
However,
the image must respect the cylinder’s ratio. It’s possible that your first attempt won’t work. But fear not, the
Vuforia system will give you the correct ratio, and you can adjust
your image proportions accordingly. For example, for a cylinder
with width of 1 and height of 2,
the ratio is 1.571. On
a target side, the image’s height is equal to the width divided by the
ratio. Once the ratio is correct, your upload will succeed.
Once
the CylinderTarget is defined, you can use it. You’ll only have to
download and import the database to Unity, and then drag a Vuforia
CylinderTarget prefab to your scene, and you’re ready to go.
5. Smart Terrain
TheSmartTerrain is a feature exclusive to Unity that enables you to
reconstruct and augment your physical environment. The feature
reconstructs, recognizes, and tracks physical objects and surfaces.
Those recognized objects can be used as terrain in Unity, opening up interesting possibilities for games and experiences.
The
experience is started when some kind of Vuforia target is
tracked. It could start when the device tracks an ImageTarget , a CylinderTarget, or any other type of target.
Once started, the system
will recognize objects distributed around the target
and set the game stage considering those objects tracked. The terrain is then
virtually projected over the scanned objects, and the experience
begins.
Smart
Terrain
can recognize simple objects like boxes and cylinders, as
long as they conform to Vuforia standards,
putting those objects directly on the game stage and using them as part of
the scene.
Those kinds of objects are called Props by the SmartTerrain system, and their size can be as small as a soup can or as
big as a large cereal box. Transparent objects such as glass are
not supported.
SmartTerrain works in three phases:
Staging:the user distributes the target and the props.
Scanning:the stage and props used in the setting are captured and
reconstructed by the Smart Terrain tracker.
Tracking:the terrain is augmented in real time by the Unity scene
you've developed.
The
creation process of a SmartTerrain experience is straightforward, but it takes a lot of
steps. Since the Vuforia team provides us with an excellent step-by-step guide to creating a Smart Terrain experience in Unity, we
won't dive into the creation process here.
If you want to give it a try,
follow the guide, and you shouldn’t have any problems. However, keep in mind that the SmartTerrain has certain limitations, especially
concerning hardware and system requirements. You can find out more about
the system in its documentation.
6. VuMark
A VuMark is a kind
of target that can be fully customized. It can reflect specific
design choices or a brand personality. It’s also extremely
recognizable by the Vuforia tracking system, and it can start AR
experiences or encode data.
Before you start to play
around with VuMark, it's good to understand that the creation process
isn’t the simplest. The Vuforia team has put
together a pretty good guide and some helpful tools, but I
would recommend this solution only for specific situations where
the design of the marker is of utmost importance.
I won't go into all the VuMark requirements and design processes. Instead, I'll just give you a general idea of the process and, if you decide to design
your own VuMarker, you’ll probably need to read Vuforia’s
guides first.
6.1 VuMark Design
It's vital to understand some
architecture and design rules for VuMark, otherwise the target
won't work properly.
A VuMark is composed of five parts:
Contour: The contour isn’t actually drawn on the VuMark; it's defined by the
contrast between the border and the clear space. It's the part that
the Vuforia’s algorithm first detects.
Border: The
most identifiable and defining shape of the VuMark. It's made
of straight lines, with at least four angles located at the
outermost edge of the design.
Clear Space: A mandatory blank area that appears adjacent to the border. It
guarantees there is enough contrast for the algorithm to detect the contour.
Code /
Elements: The visual representation of the target’s ID. This is composed of elements with high contrast, which represent two
states, dark and light. The number of elements present is defined
when creating the VuMark Template in Illustrator using the tools provided by Vuforia. (More on this in the next section.)
Background /
Design Area: This area is a blank canvas that will be ignored by the
algorithm. You can design freely on this space.
To find out more about
the design requirements, read the VuMarkDesign Guide.
6.2 Creating a VuMark
A VuMark is created using Adobe Illustrator and the VuMark Design Tools that are available for
download
on Vuforia’s site. The tools are comprised of three Illustrator
scripts, which come with some examples and a detailed PDF guide.
Before starting a VuMark, you should design its concept, considering all the guidelines proposed by Vuforia. Once the
design is complete, you’ll start to break it down into parts, obeying VuMark’s architecture.
The first step is to create a new VuMark template, using the VuMark-Setup.jsx illustrated script. In the VuMark Template
Setup window, you can choose the mark’s name, ID type, and length. The number of characters encoded in the mark will define how many elements it must contain.
Once the template is created, you must copy your design concept and break it down using the layers created by the script. Each layer represents an
architectural part and has its own rule. You’ll use the VuMark-Verify.jsx script to check if your design is correct. The
script will give you advice on how to fix the design to conform to VuMark
standards.
After breaking down your design and verifying that it conforms to VuMark standards, it's time to export the target. To
export the design, you must run the VuMark-Export.jsx script. It will create a new file in Illustrator and move the contents from your VuMark template. It will then prompt you to save
your VuMark template as an SVG file.
Finally, your marker will be ready, and you can upload the
target file to a new database in Vuforia’s Target Manager. Then
you just need to download the database and use it like any other
target.
7. Exploring Other Cool Features
It's been a long journey into the Vuforia system and the possibilities for Augmented Reality. As
you can see, the Vuforia system has a lot of cool features. This
was my fifth post on Vuforia, and yet I still couldn’t cover all its features.
However,
I’m confident that you have enough knowledge to explore those resources by yourself now.
Here are some pointers to other features
that are worth exploring:
User Defined Targets: works
like ImageTarget, but the user can define the targets on the go
using the device’s camera.
Cloud Recognition: exclusive to enterprise accounts. It's a kind of target database
that lives in the cloud, allowing you to upload and synchronize
newImageTargets with
the app on the fly.
Object Recognition: This is an experimental feature that allows the recognition of an object, after it's been scanned using a special tool provided by Vuforia.
While you're here, check out some of our other posts on AR and mobile development!
SpriteKit is Apple's 2D game engine—a rendering engine built on top of OpenGL. It was introduced with iOS 7, and each subsequent release has brought great additions to the framework. With the use of textured sprites, a built-in physics engine, and the very powerful SKAction class, you can very quickly build functional 2D games.
SpriteKit has built-in editors for scenes and particles, a camera node since the release of iOS9, and built-in support for tilesets since the release of iOS 10. With these new additions, SpriteKit is quickly becoming a powerhouse for creating 2D games.
To follow along with this tutorial, just download the accompanying GitHub repo. It has a folder called ExampleProject Starter. Open the project in that folder in Xcode, and you're ready to go!
Nodes
Nodes are the fundamental building blocks of SpriteKit, and SKNode is the base class of all nodes. All of your
onscreen assets will be an SKNode or a subclass thereof. SKNodes by
themselves do not provide any visual content, however. All visual content is
drawn using one of a number of predefined SKNode subclasses. SKNodes
and its subclasses share several properties you can alter. Some of
the more important ones are as follows.
position (CGPoint): the node's position within its parent's coordinate system
xScale (CGFloat): scales the width of a node by a multiplier
yScale(CGFloat): scales the height of a node by a multiplier
alpha (CGFloat): the transparency of the node
zRotation (CGFloat): the Euler rotation about the z axis (in radians)
One of the most important SKNodes is the SKScene. This is the root node to which all other nodes are added. By itself, SKScene does not provide any visual elements, but it displays the nodes which are added to it.
Scene Nodes
SKScenes are the root nodes to which all other nodes are added. The scene animates and renders the content from its child nodes. To display a scene, you add it to an SKView (which is a subclass of UIView and therefore has many of the same properties as UIView).
In the SpriteKit starter project, the initial scene is showing when the project loads. For now, this is just a blank black screen. It is shown when the GameViewController invokes presentScene(_:) on the view instance, passing in the scene as a parameter:
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size:CGSize(width: 768, height: 1024))
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = false
scene.scaleMode = .aspectFill
skView.presentScene(scene) // Present the Scene
}
Don't worry about the other options for now; I'll explain them later in this series.
Creating a Scene
Many games have more than one screen or scene, so we will create a new scene from scratch and then show it from our initial scene.
Select File> New> File from Xcode's menu, and choose Cocoa Touch Class.
Make sure Class is set to NewScene and that Subclass of is set to SKScene. Press Next and then Create, making sure the main target is checked. Below is the code for the NewScene.swift.
import UIKit
import SpriteKit
class NewScene: SKScene {
}
Now we have two scenes in our project, and neither has any visual content. Let's add an SKLabelNode (like all nodes, this is a subclass of SKNode). The SKLabelNode's sole purpose is to display a text label.
Label Nodes
Label nodes, implemented in the SKLabelNode class, are used to show text within your game. You can use custom fonts if you wish, but for our purposes we will just stick to the default, which displays white text and is set to Helvetica Neue Ultra Light, 32 point.
Add the following inside the didMove(to:) method within GameScene.swift. This method is called immediately after a scene is presented by a view. Generally, this is where you would set up any of your game's assets and add them to the scene.
Here we create an SKLabelNode using the convenience initializer init(text:), which takes as a parameter a string of text.
Adding and Removing Nodes
Just initializing nodes will not show them in the scene. To get the nodes to show, you have to invoke the addChild(_:) method on the receiving node, passing the SKNode that you wish to add as a parameter.
The addChild(_:) method is not exclusive to SKScenes, but is a method of SKNode. This allows you to build a complex hierarchy of nodes—known as the "node tree". For example, suppose you have a game character and you wanted to move its arms and legs separately. You could create an SKNode instance and then add each individual part as a child of that SKNode (the containing node is known as the parent node). This would give you the benefit of being able to move the character as a whole unit by moving the parent SKNode, but also allow you to move each individual part individually.
Another important method for adding nodes is the insertChild(_:at:) method, which inserts a child into a specific position within the receiver node's list of children. When you add a child to a node, the node maintains an ordered list of children which is referenced by reading the node's children property. It is important when adding multiple nodes to a parent node to take this into consideration, as the order in which you add the nodes affects some of the aspects of scene processing, including hit testing and rendering.
To remove a node, you invoke the removeFromParent() method on the node you wish to remove.
Now that we have covered adding and removing nodes, we can move our focus back to the example project. If you recall, we had just added an SKLabelNode to the GameScene. If you test now, you will see just half of the text off to the bottom left of the screen.
Why is only half the text showing, though? Now would be a good time to talk about SpriteKit's coordinate and positioning system.
Positioning and Coordinates
By default, SpriteKit's coordinate system places (0,0) at the bottom left of the screen. Also by default, SpriteKit places nodes so they are positioned at (0,0). Still, though... why are we only seeing half of the text? This is because by default the text label is centered horizontally on the label node's origin, which is (0,0). Below is an image that shows how a node's coordinate system works.
Node's origins are at (0,0), and a positive x coordinate moves to the right and a positive y coordinate goes up the screen. Remember that an SKScene is a node, and therefore its origin is also (0,0).
Setting a Node's Position
Now that we have learned SpriteKit's coordinate system works and how it places nodes, we can move the SKLabelNode to a different position so we can see all of the text. Add the following to the didMove(to:) method within GameScene.swift.
Here we position the label to the center of the scene. The position property is of type CGPoint, which has x and y values that represent a single point within the scene.
If you test now, you should see the label has been positioned in the center of the scene.
Switching Between Scenes
As it currently stands, NewScene is just a blank scene. Let's also add a label to it, and then we can learn how to switch between scenes. Here's a challenge: before you read ahead, try to add a label to NewScene that says, "Go Back". My solution is below.
The first thing we need to do is add the didMove(to:) method. Add the following to NewScene.swift.
This adds a label to NewScene with the text "Go Back". Next, we'll implement the functionality this label suggests—we'll respond to touch events by switching scenes.
Responding to Touch
Nearly all mobile games will be interacted with using touch. In this step, you will learn how to respond to touch events within your game.
To register touch event handlers within your game, you must implement the view's touchesBegan(_:with:) method. Add the following to GameScene.swift:
If you want to test this now, you will see YOU TOUCHED printed to the console when you touch on the screen. What we usually need, however, is to be able to tell when a specific node has been touched. To do this, we need some way to find and identify the nodes. We will learn how to accomplish this, and then come back and finish the touchesBegan(_:with:) method.
Searching the Node Tree
To be able to identify a node, you use the node's name property and the search the node tree for a node with that name. The node's name property takes an alphanumeric string without any punctuation.
There are a couple of methods to search for a node by its name property. If you already have a reference to the node, you can just check its name property directly, which is what we will do in the touchesBegan(_:with:) method. However, it is important to know how to search the node tree for a particular node by name, or to search for a group of nodes with the same name.
The childNode(withName:) method searches the children of a node for the specific name passed in as a parameter.
The enumerateChildNodes(withName:using:) method searches a node's children and calls the block once for each matching node it finds. You use this method when you want to find all nodes that share the same name.
The subscript(_:) method returns an array of nodes that match the name parameter.
You can also search for nodes using an advanced searching syntax that allows you to search the entire scene tree, or search for a pattern rather than an exact name, for example. This advanced searching capability is beyond the scope of this tutorial. However, if you wish to learn more, you can read about in the SKNode programming reference.
Now that we know how to search for nodes within the node tree, let's give our labels a name.
Add the following within the didMove(to:) method within GameScene.swift.
Add the following within the touchesBegan(_:with:) method within GameScene.swift.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if(touchedNode.name == "startgame"){
let newScene = NewScene(size: size)
newScene.scaleMode = scaleMode
let doorsClose = SKTransition.doorsCloseVertical(withDuration: 2.0)
view?.presentScene(newScene, transition: doorsClose)
}
}
The multiTouchEnabled property of the scene's view is set to false by default, which means the view only receives the first touch of a multitouch sequence. With this property disabled, you can retrieve the touch by using the first computed property of the touches set, since there is only one object in the set.
We can get the touchLocation within the scene from the location property of the touch. We can then figure out which node was touched by invoking atPoint(_:) and passing in the touchLocation.
We check if the touchedNode's name property is equal to "startgame", and if it is, we know that the user has touched the label. We then create an instance of NewScene and set its scalemode property to be the same as the current scene—this ensures the scene acts the same across different devices. Finally, we create an SKTransition and invoke the presentScene(_:transition:) method, which will present the scene along with the transition.
The SKTransition class has many class methods that you can invoke to show different transitions between scenes instead of immediately showing the scene. This provides a bit of "eye candy" for the end user, and makes showing a new scene seem less abrupt. To see all of the available transition types, check out the SKTransition class in the reference guide.
I'm not going to implement the touchesBegan(_:with:) method in NewScene. Why don't you try doing that on your own and have the label transition back to the GameScene using a different transition type? The code will closely resemble what we have above, only remember we named the SKLabelNode"goback".
Conclusion
We have learned a good bit about nodes so far using scenes, and you've seen how to use a label node as a generic example to learn some of the characteristics of nodes. We've studied their coordinate systems, how to locate them within the node tree, how to position them, and how to respond to touch events.
There are several other kinds of nodes available, and we'll take a look at them in the next tutorial—starting with SKSpriteNode!
To learn more about how to get started with SpriteKit, you should also check out Davis Allie's post here on Envato Tuts+.
Also, check out our SpriteKit courses! These will take you through all the steps of building your first SpriteKit game for iOS, even if you've never coded with SpriteKit before.
In this series, we're learning how to use SpriteKit to build 2D games for iOS. In this post, we'll continue our exploration of SpriteKit nodes, and learn about a special kind of node called a "sprite"—an SKSpriteNode.
To follow along with this tutorial, just download the accompanying GitHub repo. It has a folder called ExampleProject Starter. Open the project in that folder in Xcode, and you're ready to go!
Sprite Nodes
An SKSpriteNode is drawn either as a rectangle with a texture mapped onto it, or as a colored untextured rectangle. Creating a SKSpriteNode with a texture mapped onto it is the most common, as this is how you bring your game's artwork to life.
Add the following to the didMove(to:) method within GameScene.swift.
Here we are using the convenience intiailizer init(color:size:) which will draw a rectangular sprite with the color and size you pass in as parameters. If you test the project now, you will see half of the red square showing.
You might be wondering why only half of the sprite is showing since we already determined that SKNode's origins are at (0,0). This because the SKSpriteNode's frame and therefore its texture is centered on its position. To change this behaviour, you can change the sprite's anchorPoint property, which determines the point at which the frame is positioned. The diagram below shows how the anchorPoint works.
The anchorPoint is specified in the unit coordinate system, which places (0,0) at the bottom left and (1,1) at the top right corner of the frame. The default for SKSpriteNodes is (0.5,0.5).
Go ahead and change the anchorPoint property to (0,0) and notice the difference it makes.
Notice how we had to subtract from both the x and y values to center the sprite. If we had left the anchorPoint at its default then it would already have been centered on the x axis. It is important to remember that when you change the anchor point, you may have to make some adjustments in your positioning.
Textured Sprites
That red box is good for practice with positioning, but you'll usually want to texture your sprite with artwork for your game.
Let's create a textured SKSpriteNode. Add the following code at the bottom of the didMove(to:) method.
Here we use the convenience initializer init(imageNamed:), which takes as a parameter the name of an image without the extension. This is the easiest way to create a textured sprite as it creates the texture for you from the image you pass in.
The other way to create a textured SKSpriteNode is to create an SKTexture beforehand, and use one of the intializers that take a texture as a parameter.
Let's create a couple more SKSpriteNodes and change some of their properties. Again, add these to the bottom of your didMove(to:) function.
Here we create two SKSpriteNodes, enemy1 and enemy2. We set the xScale on enemy1 to 2 and change the zRotation on enemy2 to rotate it by 90 degrees. (The zRotation property takes it values in radians, and a positive value indicates a counterclockwise rotation.)
We've experimented with changing a few properties on a sprite. Take a look at the documentation for SKNodes and SKSpriteNodes and try changing a few of the other properties to see the effects they have.
Sprite nodes are good for basic rectangles and textures, but sometimes a more complex shape will be needed. The SKShapeNode has you covered in those cases. We'll take a look at shape nodes next.
Shape Nodes
Another useful node is the SKShapeNode. This node renders a shape defined by a Core Graphics path. SKShapeNodes are useful for content that cannot be easily realized with an SKSpriteNode. This class is more memory intensive and has lower performance than using an SKSpriteNode, so you should try to use it sparingly.
To assign a shape to SKShapeNode, you can set a CGPath to the node's path property. However, there are a few initializers that offer predefined shapes such as rectangles, circles, and ellipses. Let's create a circle using the convenience initializer init(circleOfRadius:).
Then, add the following to the bottom of the didMove(to:) method.
We change a few properties on the shape node, position it, and add it to the scene. It is very easy to use the predefined shape initializers. However, creating a complex CGPath manually takes a considerable amount of time and is not for the faint of heart as it usually involves some complex math.
Thankfully, there is a tool that lets you draw shapes visually and export their CGPath as Swift code. Check out PaintCode if you want to learn more.
Sprite Nodes and Shape Nodes will cover most cases, but sometimes you may wish to display video in your apps. The SKVideoNode, which we'll take a look at next, has you covered.
Video Nodes
The last node we will be taking a look at is the SKVideoNode. As the name implies, this node allows you to play video within your games.
There are a few different ways to create an SKVideoNode. One uses an instance of an AVPlayer, another just uses the name of a video file that is stored in the app bundle, and the third way is to use a URL.
One thing to keep in mind is that the video's size property will initially be the same as the size of the target video. You can change this size property, though, and the video will be stretched to the new size.
Another thing to be aware of is that the SKVideoNode offers play() and pause() methods only. If you wanted more control over your videos, you would initialize an SKVideoNode with an existing AVPlayer and use that to control your videos.
Let's use the simplest method to create an SKVideoNode. Add the following to the bottom of the didMove(to:) method.
override func didMove(to view: SKView) {
...
let video = SKVideoNode(fileNamed: "video.mov")
video.position = CGPoint(x: size.width/2,y: 600)
addChild(video)
video.play()
}
Here we used the intiailizer init(fileNamed:) to create a video. You pass in the video's name along with the extension. I have not included a video along with the project's source code, but if you want to see this work, you can add a video named "video.mov" to your project.
Conclusion
This completes our study on nodes. After reading this post and the previous one, you should have a good understanding of SKNodes and their subclasses. In the next part of this series, we will take a look at SKActions and using physics within our games. Thanks for reading, and I will see you there!
In the meantime, check out some of our other great courses and tutorials on creating iOS apps with Swift and SpriteKit.
Also, check out our SpriteKit courses! These will take you through all the steps of building your first SpriteKit game for iOS, even if you've never coded with SpriteKit before.
Unity is a multi-platform game engine developed by Unity Technologies and is used to create video games and applications for a multitude of devices (PC, consoles, mobile devices, and even websites). Unity's core advantages are its robustness and portability; Unity targets several known APIs such as Direct3D, OpenGL, OpenGL ES, and the recent Vulkan.
Due to the aforementioned characteristics, Unity has become more and more popular among AAA software development houses and aspiring game programmers.
Unity supports several technologies and components. One of the key components is the terrain engine system. Unity's terrain system allows you to create vast landscapes for your games or applications. You can use a selection of tools available to create terrains easily and quickly.
This tutorial will focus on explaining how terrain engine tools work, and how to use them to create vast and rich terrain environments.
Prerequisites
First, ensure you have the latest version of Unity. In this tutorial we're using version 5.6. Make sure that you are using the latest Unity version; otherwise you may find small differences following the tutorial and using the physics joints.
For this tutorial, you will not use any starter file. The goal is to create a new project and perform the tutorial from there.
The Terrain Tools
The first step is to create a new Unity project. Now add a new scene (File > New Scene). The new scene has been created with two game objects, the Main Camera and a Directional Light (Unity 5 will automatically add a skybox into the scene).
Now, in order to add a terrain game object, go to the GameObject menu, and select 3D Object > Terrain. This will add a flat plane (called Terrain) into your scene. This plane is the mesh that we are going to sculpt in order to create your level terrain.
If you check your Assets directory (bottom part of Unity), you will see that a New Terrain file was created.
Select the Terrain and look at the Inspector tab. You will see that the terrain has three components: Transform, the Terrain script, and the Terrain Collider component.
The terrain component provides you the tools you need to edit your terrain. All the tools on the toolbar, with the exception of the tree placement tool and the settings panel, provide a set of "brushes" and settings for brush size and opacity, just like painting tools from a normal image editor. This allows you to sculpt your terrain similarly to a painting tool.
If you select any brush under the Terrain component and mouse over the terrain, you will see a blue area projected on the terrain. This lets you know the area of the terrain that your brush will affect.
As you may have already realised, the Terrain is quite big. To navigate through the Scene, you can use the mouse and keyboard. Inside the Scene tab, press and hold your Right mouse button (the cursor should change to an eye).
While holding the mouse button:
You can change the angle of your view by moving the mouse.
Use the W, S, A, D keys to move around the 3D scene.
Before you start exploring the terrain tools, let’s take a look at the Terrain properties. The last button on the Terrain component (inside the Inspector) will show you the base parameters of the terrain.
The properties are divided into sections:
Base Terrain
Tree & Detail Objects
Wind Settings for Grass
Resolution
Heightmap
Inside the first section Base Terrain, you will find several properties and parameters.
The Draw option toggles the rendering of the terrain on or off. The Pixel Error represents the accuracy value of the mapping between the terrain maps (for example textures, heightmaps, and generated terrain). Higher values represent lower accuracy, and lower values will result in rendering overhead. It is your responsibility to balance this value in order to create a nice, rich environment.
The Base Map dist. is the maximum distance at which terrain textures will be displayed at full resolution. You can set shadows on or off by selecting Cast Shadows.
Next, we have Material. Here you set the material that is going to be used to render the terrain. This will affect how the color channels of a terrain texture are interpreted. You can select:
Built In Standard represents the Physically-Based Rendering material that was introduced in Unity 5. If you select this option, for each splat layer, you can use one texture for albedo and smoothness, one texture for normal, and one scalar value to tweak the metalness. Keep in mind that if "Overwrite Smoothness" is checked, instead of reading from texture maps, the smoothness of the entire terrain will be controlled only by the Smoothness value.
Built In Legacy Diffuse represents the legacy built-in terrain material from previous Unity releases (Unity 4 and backwards). It uses a Lambert lighting model and has optional normal map support.
Built In Legacy Specular uses the Blinn-Phong lighting model (please consult this previous Unity 5 lightning tutorial), and has optional normal map support. This option also allows you to specify the overall specular color and shininess for the terrain.
Custom option uses a custom material of your choice to render the terrain. This material should be a shader that is orientated for terrain rendering. This is an option that I only recommend to advanced users. However, if you want to give it a try, take a look at the source code of some of the built-in terrain shaders and make modifications on top of them.
You can also set Reflection Probes for some materials, but they will only be used when you’re using a built-in standard material or a custom material which supports rendering with reflection. The options for reflection probes are:
Off: Reflection probes are disabled, and only the skybox will be used for reflection.
Blend Probes: Reflection probes are enabled. The blending occurs only between probes. Default reflection will be used if there are no reflection probes nearby, but there will be no blending between default reflection and probe.
Blend Probes And Skybox: Reflection probes are enabled. Blending occurs between probes or probes and default reflection.
Simple: Reflection probes are enabled, but no blending will occur between probes when there are two overlapping volumes.
Finally, the last parameter in this section is Thickness. This value specifies how much the terrain collision volume should extend along the negative Y-axis. Objects are considered colliding with the terrain from the surface to a depth equal to the thickness. This helps to prevent high-speed moving objects from penetrating into the terrain without using expensive continuous collision detection.
The next settings section is Tree & Detail Objects.
The first option is Draw. Here, you can set the draw of the details on or off. Basically, it tells the engine if you want trees and grass to be drawn or not. Next, you have Bake Light Probes for trees. If you enable this option, Unity will create light probes at the position of each tree. The probes will then be applied to the rendering of the trees. If you disable this option, trees will still be affected by light group probes. Take note that this option is only available for trees that have light probes enabled on their prefabs.
The Detail Distance is the distance from the camera beyond which details will be culled. Detail Density representsthe number of detail objects in a given unit of area. Tree Distance represents the distance from the camera beyond which trees will be culled. The Billboard Start is the distance from the camera at which 3D tree objects will be replaced by billboard images. The Fade Length represents the distance over which trees will transition between 3D objects and billboards. Finally, the Max Mesh Trees is the maximum number of visible trees that will be represented as solid 3D meshes.
Moving on, you'll see the Wind Settings for Grass section.
Speed defines the speed of the wind as it blows grass. The Size represents the ripple size on grassy areas. Bendinglets you definethe degree to which grass objects are bent by the wind. Grass Tint lets you select the overall color tint applied to grass objects.
Inside the Resolution section you will find several resolution properties, namely the Terrain Width, Length, and Height. All three are very explicit of their meaning. The Heightmap Resolution is the pixel resolution of the terrain’s height map. It should be a power of 2 plus 1, for example, 513, where 512 is a power of 2, and you add 1.
Detail Resolution lets you define the resolution of the map. Higher resolutions will give you smaller and more detailed grass patches. The Detail Resolution per Patch sets the length and width of the square of patches rendered on a single draw call. The Control Texture Resolution is the resolution of the splat map that controls the blending of the different terrain textures. Finally, the Base Texture Resolution sets the resolution of the composite texture used on the terrain when viewed from a distance superior to the base map distance.
The last section is Heightmap. Here you can import a heightmap raw file or export your current terrain into a heightmap raw file. This can be very useful if you want to use external third-party software to interact with your application or game.
Height Tools
Before you move on to the height tools, remember that you must set the resolution settings first. Otherwise, if you try to change them later, all of the sculpting of the terrain will be lost.
The first three tools on the Terrain Inspector toolbar are the height tools. These tools are used to paint height changes onto the terrain.
They provide a set of "brushes" with settings for Brush Size and Opacity; these tools are very similar to painting tools from any image editor. This allows you to paint detail in the terrain, as if you were painting an image.
If you move your cursor to the Scene tab, you will see a blue area projected on the terrain. This lets you know the area of the terrain that your brush will affect.
The first tool is Raise / Lower Height. When painting with this tool, the height of the terrain will be increased as you sweep the mouse over it. If you hold the mouse in one particular area, the height will accumulate. Such an effect can be visualized in the following image.
To lower the terrain, just hold the Shift key while painting with the tool, and this will lower the height of your terrain.
The second tool on the terrain editor is Paint Height. This tool is used to set a specific height for an area of the terrain.
It works in a similar way to the Raise/Lower tool, but it has an additional property to set the target Height. You can select the desired Height by changing the value of the Height parameter. The Flatten button next to the height property will level the whole terrain to the chosen Height.
Finally, the third tool is Smooth Height. This tool will not drastically increase or decrease the height of the terrain; instead, it will soften the landscape and reduce the appearance of abrupt changes. This tool is particularly useful when you have painted detail using one of the noisier brushes, since these brushes tend to create sharp, jagged rocks in the landscape, and you can use this tool to soften them.
It is now time to take some time and create your own sculpted terrain. Try to use the aforementioned three tools to create a nice mountain terrain.
Paint Texture Tool
With the terrain sculpted, is now time to texture it.
Before moving on, you need to import the environment assets that are available with Unity 5. To do that, select Assets > Import Package, and select Environment. Click Import to import all environmental assets.
Unity allows you to paint your terrains with textures. You can paint your terrain with different textures in different areas. For example, you can have a texture for rock, another for sand, and another for grass. The textures can be applied with different transparency, which means that you can have smooth transitions between two textures. In order to achieve the best results, you should paint your terrain with seamless textures.
As you may have noticed on the Inspector tab, the Paint Texture tool has several brushes, and lets you change the Brush Size, the Opacity, and the Target Strength.
The first time you use the paint Texture Tool, there won’t be any textures to choose from. The first thing you need to do is to add a new texture into your pallet. To do this, click on the Edit Textures button and select Add Texture. A new window will appear.
As you can see, on this window you can Select the texture you want to use and its normal map, if you have one. Click on the square under texture and select your texture. A window will pop up to let you select the texture. Select the SandAlbedo texture and click Add.
Your terrain should now have a sand look. Note that you can always modify it on the edit texture option, or remove it.
The first texture you create should always be the base texture for the terrain. The complete list of textures used in this example are:
SandAlbedo
GrassHillAlbedo
GrassRockAlbedo
MudRockyAlbedo
CliffAlbedo
All of them are part of the environmental imported assets and can be found in the same way as the SandAbedo texture. Go ahead and import them all.
When you have all the textures imported, it is now time to texture your terrain as you want. Select a texture and paint the desired environment. An example of a textured map is the following.
When you are satisfied with the result, move on to the next section.
Paint Tree Tool
Now that you have your terrain painted, it’s time to add some trees to it. Unity 3D allows you to put trees on your terrain in the same way as you painted the textures. Using billboarding settings for distant trees, Unity maintains a good rendering performance, allowing you to have dense forests with thousands of trees.
To start painting trees, select your Terrain from the Hierarchy tab, and on the Inspector tab, select the fifth button.
Similar to textures, you also need to add the trees. Click on Edit Trees > Add Tree.
Unity will ask you for the prefab of the tree you want to use. Click on the small circle, and another window will show up. This time, it will show you all the prefabs you have in your project.
Select the Broadleaf_desktop tree prefab and click Add. This will close the prefab window and add the selected tree into the project.
You can now select the palm tree to paint it onto your terrain.
Under the Settings, you will find several parameters that you can adjust while you are placing trees in your scene. Brush Size defines the area you will paint, while Tree Density defines the number of trees you will have in the painted area. High values will mean more trees. Tree Height will allow you to define how the height changes. You can set it to random and use an interval of values, or you can set a specific value. You can Lock Width to Height of the trees, in order to make them have the same aspect ratio. You can also randomize the tree rotation orientation using the Random Tree Rotation option. Also, the tree rotation can be set to random.
Inside the Lighting section you can set how the lightmaps are generated for your trees.
In order to paint the trees in the terrain, move the mouse over into your scene view. The blue area represents the brush. With a tree type selected, press the left mousebutton on the terrain and start creating trees.
To erase trees from a certain area, hold Shift and use the left mouse button. This will clear all trees in the brush area. If you just want to erase a certain type of tree, hold Control instead of Shift. This will clear just the selected tree type.
You can always change or delete a type of tree by selecting it and clicking on the edit tree button. To modify a tree, click on edit, and to delete it, click on remove.
Finally, you will notice that inside the Terrain Collider component, you have a field named Enable Tree Colliders. By selection this option, you are enabling collisions between other game objects and the trees.
If you don’t want to paint the trees manually, you can use the Mass Place Trees button. If you click there, a new window will pop up. Unity will then ask you for a number of trees to create. Set the Number of Trees and click on Place. Unity will then randomly place the trees in your terrain.
The next step is being creative and populating your terrain with trees. In order to create different trees, you can also add new trees like the Palm_Desktop, Broadleaf_Mobile and Conifer_Desktop. For that, use the option Edit Trees under the Trees menu (inside Inspector).
You can create something like the following image (or something completely different).
When you are ready, you can move on to the next step.
Paint Details Tool
Unity allows the terrains to have grass clumps and other small objects, such as rocks, covering its surface. The grass is rendered using 2D images, while other objects are usually standard meshes.
To add details to the terrain, select your terrain object from the hierarchy. Inside the Inspector tab, click on the sixth button, the one with the flowers.
As you can see, you now have a similar interface to the Height Tools. Here you also have a set of brushes that you can choose from in order to paint a list of details.
Since you don't have any object details selected, let's add them by clicking on Edit Details > Add Grass Texture.
This will open a window where you can set several properties for your grass. Detail Texture lets you define the texture that you will use for your grass. You can also define several properties like Max Width, Min Width, Max Height, Min Height, Noise Spread, Healthy Color, Dry Color, and Billboard. All these properties will define how your grass will look.
For this tutorial, you can leave the default options or change them at your will.
Click on the circle in front of the Detail Texture field. This will open a new window. Search for the GrassFrond01AlbedoAlpha, select it, and click Add.
Just like for the trees, you can use several brushes to paint the grass. You can set different Brush Sizes and change the brush Opacity and the Target Strength.
It is now time to be creative and paint grass onto the terrain.
If you want to add a mesh to paint detail on your terrain, for example rocks, you need the repeat the same process you did with importing the grass. However, this time, when you click on Edit Details, choose Add Detail Mesh. This will open a window where you can specify several parameters of the detail mesh, very similar to the one you used to import the grass.
Unfortunately, the environment assets do not include any rock prefab, so you will not be able to paint rocks directly. However, you can search for some rocks over at the Unity Asset Store, since it is a great place to find free assets.
Conclusion
This concludes the tutorial about Unity Terrain Tools. You learned about several terrain tools and configurations. With this knowledge, you can now create, modify and improve current or new terrains for your next cutting-edge game or application.
Unity has an active economy. There are many other products that help you build out your project. The nature of the platform also makes it a great option from which you can improve your skills. Whatever the case, you can see everything we have available in the Envato Marketplace.
If you have further questions or comments, as always, feel free to drop a line in the comments section.
In this tutorial, I will try to introduce the interesting world of hexagonal tile-based games using the easiest of approaches. You will learn how to convert a two-dimensional array data to a corresponding hexagonal level layout on screen and vice versa. Using the information gained, we will be creating a hexagonal minesweeper game in two different hexagonal layouts.
This will get you started with exploring simple hexagonal board games and puzzle games and will be a good starting point to learn more complicated approaches like the axial or cubic hexagonal coordinate systems.
1. Hexagonal Tiles and Layouts
In the current generation of casual gaming, we don't see many games which use a hexagonal tile-based approach. Those we come across are usually puzzle games, board games, or strategy games. Also, most of our requirements are met by the square grid approach or isometric approach. This leads to the natural question: "Why do we need a different and obviously complicated hexagonal approach?" Let's find out.
Advantages of the Hexagonal Approach
So what makes the hexagonal tile-based approach relevant, since we already have other approaches learned and perfected? Let me list some of the reasons.
Smaller number of neighbour tiles: When compared to a square grid, which will have eight neighbour tiles, a hexagonal tile will only have six neighbours. This reduces computations for complicated algorithms.
All neighbour tiles are at the same distance: For a square grid, the four diagonal neighbours are far away when compared to the horizontal or vertical neighbours. Neighbours being at equal distances is a great relief when we are calculating heuristics and reduces the overhead of using two different methods to calculate something depending on the neighbour.
Uniqueness: These days, millions of casual games are coming out and are competing for the player's time. Great games are failing to get an audience, and one thing that can be guaranteed to grab a player's attention is uniqueness. A game using a hexagonal approach will visually stand out from the rest, and the game will seem more interesting to a crowd who are bored with all the conventional gameplay mechanics.
I would say the last reason should be enough for you to master this new approach. Adding that unique gameplay element over your game logic could make all the difference and enable you to make a great game.
The other reasons are purely technical and would only come into effect once you are dealing with complicated algorithms or larger tile sets. There are many other aspects also which can be listed as advantages of the hexagonal approach, but most of them will depend on the player's personal interest.
Layouts
A hexagon is a polygon with six sides, and a hexagon with all sides having the same length is called a regular hexagon. For theory purposes, we will consider our hexagonal tiles to be regular hexagons, but they could be squashed or elongated in practice.
The interesting thing is that a hexagon can be placed in two different ways: the pointy corners could be aligned vertically or horizontally. When pointy tops are aligned vertically, it is called a horizontal layout, and when they are aligned horizontally, it is called a vertical layout. You may think that the names are misnomers with respect to the explanation provided. This is not the case as the naming is not done based on the pointy corners but the way a grid of tiles gets laid out. The image below shows the different tile alignments and corresponding layouts.
The choice of layout entirely depends on your game's visuals and gameplay. Yet your choice does not end here as each of these layouts could be implemented in two different ways.
Let's consider a horizontal hexagonal grid layout. Alternative rows of the grid would need to be horizontally offset by hexTileWidth/2. This means we could choose to offset either the odd rows or the even rows. If we also display the corresponding row, column values, these variants would look like the image below.
Similarly, the vertical layout could be implemented in two variations while offsetting alternative columns by hexTileHeight/2 as shown below.
2. Implementing Hexagonal Layouts
From here on onwards, please start referring to the source code provided along with this tutorial for better understanding.
The images above, with the rows and columns displayed, make it easier to visualise a direct correlation with a two-dimensional array which stores the level data. Let's say we have a simple two-dimensional array levelData as below.
var levelData=
[[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0]
]
To make it easier to visualise, I will show the intended result here in both vertical and horizontal variations.
Let's start with horizontal layout, which is the image on the left side. In each row, if taken individually, the neighbour tiles are horizontally offset by hexTileWidth. Alternative rows are horizontally offset by a value of hexTileWidth/2. The vertical height difference between each row is hexTileHeight*3/4.
To understand how we arrived at such a value for the height offset, we need to consider the fact that the top and bottom triangular portions of a horizontally laid out hexagon are exactly hexTileHeight/4.
This means that the hexagon has a rectangular hexTileHeight/2 portion in the middle, a triangular hexTileHeight/4 portion on top, and an inverted triangular hexTileHeight/4 portion on the bottom. This information is enough to create the code necessary to lay out the hexagonal grid on screen.
var verticalOffset=hexTileHeight*3/4;
var horizontalOffset=hexTileWidth;
var startX;
var startY;
var startXInit=hexTileWidth/2;
var startYInit=hexTileHeight/2;
var hexTile;
for (var i = 0; i < levelData.length; i++)
{
if(i%2!==0){
startX=2*startXInit;
}else{
startX=startXInit;
}
startY=startYInit+(i*verticalOffset);
for (var j = 0; j < levelData[0].length; j++)
{
if(levelData[i][j]!=-1){
hexTile= new HexTile(game, startX, startY, 'hex',false,i,j,levelData[i][j]);
hexGrid.add(hexTile);
}
startX+=horizontalOffset;
}
}
With the HexTile prototype, I have added some additional functionalities to the Phaser.Sprite prototype which enables it to display the i and j values. The code essentially places a new hexagonal tile Sprite at startX and startY. This code can be changed to display the even offset variant just by removing an operator in the if condition like this: if(i%2===0).
For a vertical layout (the image on the right half), neighbour tiles in every column are vertically offset by hexTileHeight. Each alternate column is vertically offset by hexTileHeight/2. Applying the logic which we applied for vertical offset for the horizontal layout, we can see that the horizontal offset for the vertical layout between neighbour tiles in a row is hexTileWidth*3/4. The corresponding code is below.
var verticalOffset=hexTileHeight;
var horizontalOffset=hexTileWidth*3/4;
var startX;
var startY;
var startXInit=hexTileWidth/2;
var startYInit=hexTileHeight/2;
var hexTile;
for (var i = 0; i < levelData.length; i++)
{
startX=startXInit;
startY=2*startYInit+(i*verticalOffset);
for (var j = 0; j < levelData[0].length; j++)
{
if(j%2!==0){
startY=startY+startYInit;
}else{
startY=startY-startYInit;
}
if(levelData[i][j]!=-1){
hexTile= new HexTile(game, startX, startY, 'hex', true,i,j,levelData[i][j]);
hexGrid.add(hexTile);
}
startX+=horizontalOffset;
}
}
In the same way as with the horizontal layout, we can switch to the even offset variant just by removing the ! operator in the top if condition. I am using a Phaser Group to collect all the hexTiles named hexGrid. For simplicity, I am using the centre point of the hexagonal tile image as an anchor, or else we would need to consider the image offsets as well.
One thing to notice is that the tile width and tile height values in the horizontal layout are not equal to the tile width and tile height values in the vertical layout. But when using the same image for both layouts, we could just rotate the tile image 90 degrees and swap the values of tile width and tile height.
3. Finding the Array Index of a Hexagonal Tile
The array to screen placement logic was interestingly straightforward, but the reverse is not so easy. Consider that we need to find the array index of the hexagonal tile on which we have tapped. The code to achieve this is not pretty, and it is usually arrived at by some trial and error.
If we consider the horizontal layout, it may seem that the middle rectangular portion of the hexagonal tile can easily help us figure out the j value as it is just a matter of dividing the x value by hexTileWidth and taking the integer value. But unless we know the i value, we don't know if we are on an odd or even row. An approximate value of i can be found by dividing the y value by hexTileHeight*3/4.
Now come the complicated parts of the hexagonal tile: the top and bottom triangular portions. The image below will help us understand the problem at hand.
The regions 2, 3, 5, 6, 8, and 9 together form one tile. The most complicated part is to find if the tapped position is in 1/2 or 3/4 or 7/8 or 9/10. For this, we need to consider all the individual triangular regions and check against them using the slope of the slanted edge.
This slope can be found from the height and width of each triangular region, which respectively are hexTileHeight/4 and hexTileWidth/2. Let me show you the function which does this.
function findHexTile(){
var pos=game.input.activePointer.position;
pos.x-=hexGrid.x;
pos.y-=hexGrid.y;
var xVal = Math.floor((pos.x)/hexTileWidth);
var yVal = Math.floor((pos.y)/(hexTileHeight*3/4));
var dX = (pos.x)%hexTileWidth;
var dY = (pos.y)%(hexTileHeight*3/4);
var slope = (hexTileHeight/4)/(hexTileWidth/2);
var caldY=dX*slope;
var delta=hexTileHeight/4-caldY;
if(yVal%2===0){
//correction needs to happen in triangular portions & the offset rows
if(Math.abs(delta)>dY){
if(delta>0){//odd row bottom right half
xVal--;
yVal--;
}else{//odd row bottom left half
yVal--;
}
}
}else{
if(dX>hexTileWidth/2){// available values don't work for even row bottom right half
if(dY<((hexTileHeight/2)-caldY)){//even row bottom right half
yVal--;
}
}else{
if(dY>caldY){//odd row top right & mid right halves
xVal--;
}else{//even row bottom left half
yVal--;
}
}
}
pos.x=yVal;
pos.y=xVal;
return pos;
}
First, we find xVal and yVal the same way we would do for a square grid. Then we find the remaining horizontal (dX) and vertical (dY) values after removing the tile multiplier offset. Using these values, we try to figure out if the point is within any of the complicated triangular regions.
If found, we make corresponding changes to the initial values of xVal and yVal. As I have said earlier, the code is not pretty and not straightforward. The easiest way to understand this would be to call findHexTile on mouse move, and then put console.log inside each of those conditions and move the mouse over various regions within one hexagonal tile. This way, you can see how each intra-hexagonal region is handled.
The code changes for the vertical layout are shown below.
function findHexTile(){
var pos=game.input.activePointer.position;
pos.x-=hexGrid.x;
pos.y-=hexGrid.y;
var xVal = Math.floor((pos.x)/(hexTileWidth*3/4));
var yVal = Math.floor((pos.y)/(hexTileHeight));
var dX = (pos.x)%(hexTileWidth*3/4);
var dY = (pos.y)%(hexTileHeight);
var slope = (hexTileHeight/2)/(hexTileWidth/4);
var caldX=dY/slope;
var delta=hexTileWidth/4-caldX;
if(xVal%2===0){
if(dX>Math.abs(delta)){// even left
}else{//odd right
if(delta>0){//odd right bottom
xVal--;
yVal--;
}else{//odd right top
xVal--;
}
}
}else{
if(delta>0){
if(dX<caldX){//even right top
xVal--;
}else{//odd mid
yVal--;
}
}else{//current values wont help for even right bottom
if(dX<((hexTileWidth/2)-caldX)){//even right bottom
xVal--;
}
}
}
pos.x=yVal;
pos.y=xVal;
return pos;
}
4. Finding Neighbours
Now that we've found the tile on which we have tapped, let's find all six neighbouring tiles. This is a very easy problem to solve once we visually analyse the grid. Let's consider the horizontal layout.
The image above shows the odd and even rows of a horizontally laid out hexagonal grid when a middle tile has the value of 0 for both i and j. From the image, it becomes clear that if the row is odd, then for a tile at i,j the neighbours are i, j-1, i-1,j-1, i-1,j, i,j+1, i+1,j, and i+1,j-1. When the row is even, then for a tile at i,j the neighbours are i, j-1, i-1,j, i-1,j+1, i,j+1, i+1,j+1, and i+1,j. This could be manually calculated easily.
Let's analyse a similar image for the odd and even columns of a vertically aligned hexagonal grid.
When we have an odd column, a tile at i,j will have i,j-1, i-1,j-1, i-1,j, i-1,j+1, i,j+1, and i+1,j as neighbours. Similarly, for an even column, the neighbours are i+1,j-1, i,j-1, i-1,j, i,j+1, i+1,j+1, and i+1,j.
5. Hexagonal Minesweeper
With the above knowledge, we can try to make a hexagonal minesweeper game in the two different layouts. Let's break down the features of a minesweeper game.
There will be N number of mines hidden inside the grid.
If we tap on a tile with a mine, the game is over.
If we tap on a tile which has a neighbouring mine, it will display the number of mines immediately around it.
If we tap on a mine without any neighbouring mines, it would lead to the revealing of all the connected tiles which do not have mines.
We can tap and hold to mark a tile as a mine.
The game is finished when we reveal all tiles without mines.
We can easily store a value in the levelData array to indicate a mine. The same method can be used to populate the value of nearby mines on the neighbouring tiles' array index.
On game start, we will randomly populate the levelData array with N number of mines. After this, we will update the values for all the neighbouring tiles. We will use a recursive method to chain reveal all the connected blank tiles when the player taps on a tile which doesn't have a mine as neighbour.
Level Data
We need to create a nice looking hexagonal grid, as shown in the image below.
This can be done by only displaying a portion of the levelData array. If we use -1 as the value for a non-usable tile and 0 as the value for a usable tile, then our levelData for achieving the above result will look like this.
While looping through the array, we would only add hexagonal tiles when the levelData has a value of 0. For the vertical alignment, the same levelData can be used, but we would need to transpose the array. Here is a nifty method which can do this for you.
levelData=transpose(levelData);
//...
function transpose(a) {
return Object.keys(a[0]).map(
function (c) { return a.map(function (r) { return r[c]; }); }
);
}
Adding Mines and Updating Neighbours
By default, our levelData has only two values, -1 and 0, of which we would be using only the area with 0. To indicate that a tile contains a mine, we can use the value of 10.
A blank hexagonal tile can have a maximum of six mines near it as it has six neighbouring tiles. We can store this information also in the levelData once we have added all the mines. Essentially, a levelData index having a value of 10 has a mine, and if it contains any values from 0 to 6, that indicates the number of neighbouring mines. After populating mines and updating neighbours, if an array element is still 0, it indicates that it is a blank tile without any neighbouring mines.
We can use the following methods for our purposes.
function addMines(){
var tileType=0;
var tempArray=[];
var newPt=new Phaser.Point();
for (var i = 0; i < levelData.length; i++)
{
for (var j = 0; j < levelData[0].length; j++)
{
tileType=levelData[i][j];
if(tileType===0){
newPt=new Phaser.Point();
newPt.x=i;
newPt.y=j;
tempArray.push(newPt);
}
}
}
for (var i = 0; i < numMines; i++)
{
newPt=Phaser.ArrayUtils.removeRandomItem(tempArray);
levelData[newPt.x][newPt.y]=10;//10 is mine
updateNeighbors(newPt.x,newPt.y);
}
}
function updateNeighbors(i,j){//update neighbors around this mine
var tileType=0;
var tempArray=getNeighbors(i,j);
var tmpPt;
for (var k = 0; k < tempArray.length; k++)
{
tmpPt=tempArray[k];
tileType=levelData[tmpPt.x][tmpPt.y];
levelData[tmpPt.x][tmpPt.y]=tileType+1;
}
}
For every mine added in addMines, we are incrementing the array value stored in all of its neighbours. The getNeighbors method won't return a tile which is outside our effective area or if it contains a mine.
Tap Logic
When the player taps on a tile, we need to find the corresponding array element using the findHexTile method explained earlier. If the tile index is within our effective area, then we just compare the value at the array index to find if it is a mine or blank tile.
function onTap(){
var tile= findHexTile();
if(!checkforBoundary(tile.x,tile.y)){
if(checkForOccuppancy(tile.x,tile.y)){
if(levelData[tile.x][tile.y]==10){
//console.log('boom');
var hexTile=hexGrid.getByName("tile"+tile.x+"_"+tile.y);
if(!hexTile.revealed){
hexTile.reveal();
//game over
}
}
}else{
var hexTile=hexGrid.getByName("tile"+tile.x+"_"+tile.y);
if(!hexTile.revealed){
if(levelData[tile.x][tile.y]===0){
//console.log('recursive reveal');
recursiveReveal(tile.x,tile.y);
}else{
//console.log('reveal');
hexTile.reveal();
revealedTiles++;
}
}
}
}
infoTxt.text='found '+revealedTiles +' of '+blankTiles;
}
We keep track of the total number of blank tiles using the variable blankTiles and the number of tiles revealed using revealedTiles. Once they are equal, we have won the game.
When we tap on a tile with an array value of 0, we need to recursively reveal the region with all the connected blank tiles. This is done by the function recursiveReveal, which receives the tile indices of the tapped tile.
function recursiveReveal(i,j){
var newPt=new Phaser.Point(i,j);
var hexTile;
var tempArray=[newPt];
var neighbors;
while (tempArray.length){
newPt=tempArray[0];
var neighbors=getNeighbors(newPt.x,newPt.y);
while(neighbors.length){
newPt=neighbors.shift();
hexTile=hexGrid.getByName("tile"+newPt.x+"_"+newPt.y);
if(!hexTile.revealed){
hexTile.reveal();
revealedTiles++;
if(levelData[newPt.x][newPt.y]===0){
tempArray.push(newPt);
}
}
}
newPt=tempArray.shift();//it seemed one point without neighbor sometimes escapes the iteration without getting revealed, catch it here
hexTile=hexGrid.getByName("tile"+newPt.x+"_"+newPt.y);
if(!hexTile.revealed){
hexTile.reveal();
revealedTiles++;
}
}
}
In this function, we find the neighbours of each tile and reveal that tile's value, meanwhile adding neighbour tiles to an array. We keep repeating this with the next element in the array until the array is empty. The recursion stops when we meet array elements containing a mine, which is ensured by the fact that getNeighbors won't return a tile with a mine.
Marking and Revealing Tiles
You must have noticed that I am using hexTile.reveal(), which is made possible by creating a HexTile prototype which keeps most of the attributes related to our hexagonal tile. I use the reveal function to display the tile value text and set the tile's colour. Similarly, the toggleMark function is used to mark the tile as a mine when we tap and hold. HexTile also has a revealed attribute which tracks whether it is tapped and revealed or not.
Check out the hexagonal minesweeper with horizontal orientation below. Tap to reveal tiles, and tap-hold to mark mines. There is no game over as of now, but if you reveal a value of 10, then it is hasta la vista baby!
Changes for the Vertical Version
As I am using the same image of hexagonal tile for both orientations, I rotate the Sprite for the vertical alignment. The below code in the HexTile prototype does this.
if(isVertical){
this.rotation=Math.PI/2;
}
The minesweeper logic remains the same for the vertically aligned hexagonal grid with the difference for findHextile and getNeighbors logic which now need to accommodate the alignment difference. As mentioned earlier, we also need to use the transpose of the level array with corresponding layout loop.
Check out the vertical version below.
The rest of the code in the source is simple and straightforward. I would like you to try and add the missing restart, game win, and game over functionality.
Conclusion
This approach of a hexagonal tile-based game using a two-dimensional array is more of a layman's approach. More interesting and functional approaches involve altering the coordinate system to different types using equations.
The most important ones are axial coordinates and cubic coordinates. There will be a follow-up tutorial series which will discuss these approaches. Meanwhile, I would recommend reading Amit's incredibly thorough article on hexagonal grids.
Countdowns are simple elements that appear in a lot of games, yet their design can be easily overlooked.
With a few simple additions, though, a regular timer can be made much more engaging and fitting the universe of the game presented, and thus greatly enhance the player's experience.
A lot of these elements are inspired by the talk "Juice it or lose it" by Martin Jonasson and Petri Purho on how to make your game "juicier". The bottom line is that it is not enough to just present the information in its most base form; you also need to enhance and accentuate what is happening with color, sounds and movement, which will make the entire experience much more engaging. I totally recommend watching that talk to also learn about other applications.
In this two-part series, we will go through these elements and then
implement them in a simple Unity project, for which you will need the newest version. In the upcoming part 2, we will look at more detailed and nuanced elements.
Ready? Let's go!
The Purpose of Countdowns
Timers in games can serve different purposes, which have not necessarily overlapping applications. These are:
You have X amount of time to accomplish a task.
You need to survive for X amount of time.
The design of a timer can change between those two, as the information necessary for the player changes in its application.
When you are given five minutes to defuse a bomb in Metal Gear Solid 2, you need to know the exact amount of time you have, as every second becomes vital to the overall progress and planning. The timer needs to convey to the player how much is left, and to alert them if that amount is becoming critically low.
When you have to hold out for a certain amount of time, that element of exactitude becomes less important. While it would of course be nice to know the exact amount of time in seconds you need to survive, the game can also work without that information. It could even be improved, as a piece of information like "you need to hold out for just a few seconds more!" is more easily understood in a stressful situation than "please read this number readout in the corner of the screen", and can enhance the immersion within the gameworld.
How to Improve Countdowns
Now let's take a look at elements that will make countdowns and timers more interesting and engaging.
1: Create Your Own Timer That Doesn't Rely on Numbers and Embed the Timer Into the World
Instead of just putting a number somewhere, you can create your own countdown graphic which will tell the user roughly how much time is left.
A graphical, 2D implementation will require some finesse, as you need to make sure the player can correctly intuit and read the idea of "time is running out and you've got roughly X seconds left".
This works very well if you use environmental indicators to show the progress,
and have in-game voices tell you how much is left. A number pasted on
top of the screen can feel non-diegetic and might very well take some
players out of the game.
In Half Life 2, for example, you need to defend a position until a teleporter has charged. At the beginning, a full charge is displayed, which then has to be refilled again.
The charging is shown by elements of the machine lighting up and
starting to spin, giving you a rudimentary clock that conveys how
much longer you have to hold out.
The teleporter charge is also a good example of an embedded display, which is directly in the gameworld. These are useful as they keep the player focussed on the world itself, while a timer on a GUI layer would be on top of the gameworld.
2: Make the Numbers Look Like Numbers
Get a cool font for your countdowns! Certain fonts like this Digital Display Font emulate older lightup displays, which were mostly used for numbers in old "digital" displays.
The player will already associate these fonts with timers and countdowns, and it will make reading it much simpler, as if it were in something like Arial or Times New Roman.
Basic readability and text composition also apply. Do not stack the numbers vertically, do not use Roman numerals, etc.
3: Give the Player the Amount in Seconds, Not in Minutes
The Metal Gear Solid series does this wonderful trick where they say "you only have 500 seconds left!" and then display a number that reads as "5:00".
When I played it for the first time, my mind auto-completed this to read as "5 minutes", but it was tricked. "500 seconds" is an uncommon yet correct format for time, and people in general are not trained to correctly pick up on this.
The effect is that players will think they only have 5 minutes, while in actuality it is 8 minutes and 20 seconds.
This usually has no drawbacks, as they will be spurred by the tight time limit and concentrate on accomplishing the task (in Metal Gear Solid 2: defusing bombs), which will distract them from the fact that five minutes may have passed, and there is still time.
4: Make a "Minute" 100 Seconds Long
This goes hand in hand with telling the player the amount of time left in seconds.
A recurring element in the MGS2 countdowns is that they do not have 60-second
turnovers, but 100-second "turnovers". This is not actually a minute turning over to the next, but the seconds being listed. So instead of 01:00 becoming 00:59, it is 0100 that becomes 0099. You need to just get the value itself, which makes a countdown actually easier to create, as you can skip the minute calculation.
5: Using Milliseconds and Fractions to Add Tension
Having milliseconds displayed after the main number makes it much more interesting!
This is rather easy, as time can be stored in a float already. Two to three floating point numbers are a good amount that is still readable and potentially useful to the player.
6: Use Color to Your Advantage!
When the timer reaches a certain low amount, color the text differently. Making it yellow works well, as it makes the timer stand out more.
Once it reaches its final seconds, make it red, bigger, and flash. Now the low amount cannot be ignored, and it creates tension in the player.
Let's Build a Simple Timer With These Things!
Now let's get started on how to improve a timer via code. We will turn this:
Into this:
We will use a few special assets you can quickly get or create yourself, or get from the source files you can download on the upper right of this article.
Then create an empty gameobject, place it in front of the camera, and add a textMesh via Component > Mesh > TextMesh to it. This will be the display for the timer in the gameworld.
We'll use the 3d-textmesh for now, but this will also work on other areas, like the basic Unity interface. For now, the 3d-text also has the benefit that it can be inside the gameworld, as mentioned before, and not on top of the world inside a GUI-layer.
Our new 3d-text will not be displayed immediately; we need to alter a few values.
Set the character size to 0.1, the anchor to middle left, the alignment to center, and the font size to 80. Also add some placeholder text into the "text" field. This is just so we have something to look at in the editor; during actual gameplay, this will be directly adapted via code. 00:00 will do nicely.
Then create a new file called Countdown.cs, add it to our timer-object, and add this code to it:
using UnityEngine;
using System.Collections;
public class Countdown : MonoBehaviour {
float timer = 120f;
void Update (){
timer -= Time.deltaTime;
GetComponent<TextMesh>().text = timer.ToString();
}
}
We have a variable named timer, which will hold the time that is counting down. Right now it is at 120 seconds, or 2 minutes. Then in the Update function, which is called every frame, we deduct Time.deltaTime. This is a shortcut to get the actually elapsed time in there. Following that, we get the textMesh component from the object and adapt its text value to be the timer, turned into a string.
When you now run the game, the textmesh will show the number decreasing. Congratulations! You have built a rudimentary timer.
But it is far from being engaging yet. Also, it shows 4 to 5 numbers after the decimal point, and it will continue to go into negative values after reaching zero.
Have It Stop at Zero
A negative timer will look broken, so let's make sure that doesn't happen. Adapt the Update function to look like this:
This will only decrease the timer if it is actually positive. Should it turn negative anyway (which could happen when it subtracts the elapsed time), it will be set to zero.
Get Nicer Numbers
Remember that cool Digital Display Font I mentioned above? Go get it and add it to your project!
The textmesh section in the inspector has a field for the font, so drag the font from your assets there.
And now our timer will look much neater!
Fix the Amount of Numerals
Right now the amount of numbers after the decimal point varies, which makes the timer fluctuate wildly. Fix that by adapting the line that applies the value to the text to look like this:
And the timer will turn yellow when less than 20 seconds are remaining, and red when less than 10 seconds are remaining.
The entire Countdown.cs file should look like this:
using UnityEngine;
using System.Collections;
public class Countdown : MonoBehaviour {
float timer = 60f;
public AudioClip soundBlip;
void Update (){
if (timer > 0f){
timer -= Time.deltaTime;
} else {
timer = 0f;
}
if (timer < 10f) {
GetComponent<TextMesh>().color = Color.red;
} else if (timer < 20f) {
GetComponent<TextMesh>().color = Color.yellow;
}
GetComponent<TextMesh>().text = timer.ToString("F2");
}
}
And that's it! We now have a much more engaging and interesting countdown.
You can also look at the complete project in the source files on the upper right of this article.
Conclusion
We took a look at several elements that will improve timers in games, the lessons of which can also be applied to a lot of other elements. In addition, the timer we built can be easily adapted and slotted into any sort of game, and will be an interesting element without you having to develop its contents again.
But there's more! In the next part of this series, we will look at even more elements and continue to improve our timer.
Last time, we looked at countdowns in games, how they are set up, and what elements you can use to make them more engaging. There are much more than will fit in a single article, though!
This will continue from Part 1, which is why we will start at number seven.
Ready? Let's go!
7: Have a Different Constant Ticking Speed
This is a
simple trick that does not even necessitate you lying to the player. You
just say "when the timer runs out" and show a ticking clock. What you
do not mention is the unit that is shown.
If you show
a "10", the player will intuit this is
10 seconds, but the number can decrease slower than seconds would. If
you modify the value with a 0.5 multiplier, for example, it will run out after 20seconds instead of just 10.
The
theme of the jam necessitated that 10 seconds are used, but 10 actual
seconds are a much-too-low amount of time to accomplish something
meaningful.
8: Adapt the Ticking Speed During Gameplay
This also works better if time units are not mentioned and the player is just given a rough idea of "until this happens".
If
you have a series of panels that light up to show timer progress, you do
not need to activate them at the same rate. In fact, it will become more
intense if the first few light up quickly, and the latter ones have
more time between them. In the heat of action-packed gameplay, the player
will not realize this and will have a much more intense experience.
This
should not be employed with actual time units, as players might feel
cheated and lied to. Do not break the player's trust in your system.
This can be seen in one level of Starfox 64, where the player has to defend a position against an approaching giant ship.
On the first glance, you get a rough idea of how much time is left, but the ship itself
appears to move at different speeds and not in a straight line.
The timer is in essence being adapted on the fly, the process of which is hidden behind smoke and mirrors.
9: Use Sounds
Countdowns do not have to be purely optical! A beep sound every few seconds will greatly enhance immersion.
Once
you have that, you can also adapt the frequency once a certain amount of
time has passed. If the beep occurs every five seconds, in the last 30
seconds of the timer the player will be notified without having to look
at the number and mentally calculate how much is left.
Similarly, it also helps if a character comments on the progress. Having someone
say "We are halfway done!" gives you a well-framed piece of information
that is much easier to understand than parsing it from a readout.
10: Use Scaling Visuals
This works very well when the counter
is nearing its end. Whenever another second elapses, scale up
the entire counter for half a second.
In addition to color and sound, this will make it much juicier.
11: Do Something When It Reaches Zero
Make
sure a timer never goes negative—this will be confusing to a lot of
people and seem like a bug, as bugged, badly-created timers tend to do
that.
Having it flash would be a nice touch, as it again underlines the point of it having just run out.
If you have a positive timer, it is a fun element to just let it keep running and use that element as a high-score. The game Devil Daggers does a similar thing, where the main goal is "Survive 500 seconds".
What Not to Do That Would Break the Player's Trust
Once the rules of a timer have been established, they should remain roughly consistent,
with more leeway being allowed the less exact a timer is. The main
question should be, "How is this hidden rule benefitting the experience?"
When you slow down a timer in the last three seconds, it will make the
situation more tense and give the player a few more actual seconds to
accomplish things. Randomly slowing it down in the middle, or speeding
it up, will only break the player's trust in your rules.
Let's Build It
This will continue with the code-base from the last tutorial and build on it. If you haven't checked it out yet, do so now! You can also download the finished source on the upper right of this article.
When we last left our timer, it looked like this:
Now, new additions will make it behave more interestingly. We will continue with the countdown.cs-script, which should look like this:
using UnityEngine;
using System.Collections;
public class Countdown : MonoBehaviour {
float timer = 60f;
public AudioClip soundBlip;
void Update (){
if (timer > 0f){
timer -= Time.deltaTime;
} else {
timer = 0f;
}
if (timer < 10f) {
GetComponent<TextMesh>().color = Color.red;
} else if (timer < 20f) {
GetComponent<TextMesh>().color = Color.yellow;
}
GetComponent<TextMesh>().text = timer.ToString("F2");
}
}
Make It Beep
Let's make our timer beep every second, so that the player will know it is decreasing even without looking at it.
First, we need a nice blip sound effect. You can find an audio file in the
source files, or create a nice one using the free tool BFXR. Once you have one, copy it into your asset folder.
Add an AudioSource component to your countdown object via Component > Audio > AudioSource. Add this variable to the countdown script:
public AudioClip soundBlip;
You also need to assign the sound file to the soundBlip variable:
This will check whether the timer is at the full second mark, and if so play the sound file.
The
actual workings of this code are a bit more complicated. What it does
is first round the floating-point timer to two decimal points, then
divide it by 1, and see if there is a remainder. If the remainder is
zero, it means the timer has reached a full second, and the blip sound
can be played.
If we do not do this, the system might trigger much more often, which will not be fun and conducive to a good timer.
Different Speed
This will add a multiplier which will reduce or accelerate the speed of the ticking clock. Add this variable at the beginning:
float multiplier = 0.6f;
Now find the line that reduces the timer—it's this one:
timer -= Time.deltaTime;
And adapt it to look like this:
timer -= Time.deltaTime * multiplier;
If the multiplier is below 1.0 the timer will now run slower, and if it's above 1.0 it will run quicker. Setting it at 1.0 will do nothing.
Try to experiment to find a good value! I feel a slightly lower speed, like 0.85f, will make the player feel the ticking clock more.
Slow Down When Low
Now that we have a multiplier component, we can change it during gameplay!
Go to the color-changing block of code:
if (timer < 10f) {
GetComponent<TextMesh>().color = Color.red;
} else if (timer < 20f) {
GetComponent<TextMesh>().color = Color.yellow;
}
Here we already have the conditions where a change in ticking speed would be appropriate, so we can just add it!
When the timer turns yellow at 20 seconds, it will now tick with 80% speed. Once it turns red, it will go down to 60% regular speed. Otherwise, it will be set to 100% speed.
Scaling When Low
Another great way to make a timer running out of time stand out more is to scale it up every passing second when low. Since we already have code that gets triggered every second, we can adapt it further!
First, we need a function to increase and decrease the size of our display. Add this function:
This is an IEnumerator, which is a type of function that can contain wait commands. We need the isBlinking variable to make sure we do not call it multiple times.
Once initiated, it will scale up the size of the object by the factor of 1.4f, wait 0.3 seconds, and then scale down again to the original size.
This will get the job done. You can, of course, make it more elegant and combine the commands into a more efficient code.
Conclusion
Our standard little countdown has become much more interesting and engaging. If you build this once, you can use it and plug it into any project you make, no matter how small.
If you're already an Elements subscriber, you'll now see that, along with all the graphics, fonts, web templates, 3D models and everything else you've been used to downloading, you have unlimited access to over 240,000 high-quality stock images. Enjoy!
If you're not an Elements subscriber, here's how it works. For a single monthly subscription of US$29, you get unlimited downloads from a broad and highly curated selection of:
graphics (vectors, icons, patterns, textures, illustrations and more)
add-ons (think Photoshop and Illustrator actions, Lightroom presets, etc.)
web templates (landing pages, full websites, email newsletters, etc.)
CMS templates (Shopify, Magento, OpenCart, and a lot more)
3D models
Oh, and you also get free access to over 1,000 courses and 170 eBooks here on Envato Tuts+, as well as free AND CO membership to help you with invoicing and contracts.
And because all of that just wasn't enough, now you get almost a quarter of a million stock photos too.
Why Envato Elements?
Of course, there are loads of stock photo sites out there already, both free and paid. So why should you bother with Envato Elements? Here are a few reasons:
1. Get Premium Quality
On some other stock photo sites, the emphasis is on quantity: thousands of people uploading millions of images, with little quality control. On Envato Elements, the collection is carefully curated to make sure you have access to the very best images, without having to wade through tons of mediocre ones.
2. Get Broad Commercial Rights
No matter whether you're using photos for a website, an artwork, a client project or a professional document, you need to know that you have the right to use those images and won't get sued for copyright infringement. All the items on Envato Elements come with a simple, easy-to-understand commercial license, so that you can use them in your projects with confidence—and without having to squint at a mass of small print.
3. Find Exactly What You Need
Elements offers some useful filters to help you find what you need quickly and effectively. You can search by subject, of course, but you can also filter for image orientation (landscape, portrait, or square) and for the predominant colours in the image. So you can find images that not only convey the right message, but also fit seamlessly with your overall design.
4. Get a Lot of Other Stuff Too
As I mentioned above, you get a ton of other useful things bundled with an Elements subscription. There are no usage limits, no credits to keep track of. You just use as many photos, templates, fonts and graphics as you need for the projects you're working on. So whether you're a web designer, a developer, an artist a businessperson or a freelancer in a number of creative fields, you'll be able to get good value out of an Elements subscription.
What to Do Next
To learn more about the new selection of photos, head over to Envato Elements. You can browse the full selection of items even if you don't have an account, so take a look around to get an idea of what's on offer and decide whether it would be useful for you.
Keep in mind that Envato Elements is expanding all the time, both in terms of the number of categories being offered and the number of items in each category. So this is a subscription that's only going to get more valuable over time.
Learn Cinema 4D quickly and easily by creating a Floating Island Landscape. Follow this tutorial step by step for use in video games, graphic design and illustration projects.
You can use the popular low poly style for this project. Some of the skills you'll learn will include creating basic 3D shapes, manipulating them, and using basic lighting and rendering.
1. How to Create Low Poly Sphere
Step 1
Open Cinema 4D and take a look at the top menu bar of the screen. You'll find almost all the tools that you will need to create the floating island.
Primary-Click and Hold on the blue Add Cube Object button. This reveals a sub menu where you can click to create more objects. Select the Sphere tool by clicking on it.
Step 2
Once the sphere has been created, change the properties of the sphere in the lower right-hand corner of the screen.
Under the Object tab, scale the sphere up from 100cm to 250cm. For this tutorial, I've changed the Segments to 100 and the sphere type to Octahedron.
Step 3
Untick Render Perfect and remove the Phong Tag. Do this by clicking on it to select it and then pressing Delete or Backspace on the keyboard. This allows us to render the sphere in the low poly style (otherwise it will appear smooth when it is rendered).
Step 4
Primary-Click and Hold on the blue Bend button. This reveals a sub menu where you can click to create more objects. Select Polygon Reduction.
Step 5
Now click and drag the Polygon Reduction so that it is under Sphere. Now go to the Options tab, and place the Reduction Strength at around 92%.
Step 6
Primary-Clickand Hold on the blue Bend button to select Displacer.
Step 7
Click and drag Displacer so that it is situated above Polygon Reduction.
Step 8
With Displacer selected, go to the Shading Tab. Here you will be able to select Shader > Noise.
Step 9
Go to the Object Tab and play about with the settings here until you come up with a shape that you like. For this tutorial I have put the value at 30cm Height.
Step 10
Go to the top menu bar and select MoGraph > Effector > Random. Place Random in the Sphere Object with the others (above Displacer) and select the Deformer tab.
From there select Polygon. You can then go to the Parameter tab and the play about with the options there until you are happy with the shape.
2. How to Model a Floating Island
Step 1
In order to make any further changes to the sphere, I will need to make it editable. To do this, ensure the sphere is selected and click on the Make Editable button in the top right of the screen.
Once you do this, you will not have access to some of the previous options such as radius, segments, type, render perfect. Ensure you're happy with the sphere setup before moving on to this step.
Step 2
Ensuring the sphere is still selected, click on the Polygons button. This changes the appearance of the sphere temporarily in order to make the modelling process easier. Then click the Live Selection Tool.
Step 3
With the Live Selection Tool selected, right click and select Brush. In the options, set Falloff >Constant and set the Mode > Surface.
You can also adjust the Strength and Radius settings as well to suit your preference. For the purpose of this tutorial I will set the Strength > -25% and the Radius > 75cm.
Step 4
Use the cursor to brush over the sphere object to manipulate it into the shape that you want the island to be.
Step 5
Deselect your sphere to check out how the low poly island looks.
3. How to Cut the Floor of the Island
Step 1
Create a Cube by clicking on the cube object button and resize it so that it is large enough to cover the top half of the island. Make sure that the part of the sphere you cover is that section you wish to cut.
Step 2
Go to the top menu and select Array > Boole. Once you have selected Boole, click and drag both the Sphere and the Cube into it. Ensure the Sphere is placed above the Cube.
Step 3
Where the cube overlaps the sphere is where you will cut the floor. You can now select the cube object and move it up and down to find a spot that you like best.
4. How to Create the Mountains
Step 1
Now it is time to create the mountains. Go to the top menu again and select Cube > Landscape. This will spawn a landscape plane.
Step 2
In the options menu, experiment with the Size settings—particularly the middle option—until you create the size of the mountain that you like.
Step 3
You can also use the Scale Tool (T) to resize the mountain to the desired size.
Step 4
Delete the Phong Tag which is located on the right side of the landscape object. This will help give the mountain the Low Poly look.
Step 5
Making sure that the Landscape is still selected, make the Width Segments > 15 and the Depth Segments > 15.
Step 6
Use the Move Tool (E) to move the mountain into place. Put the base of the mountain inside the floor.
Step 7
Go to the top menu bar, select Bend > Polygon Reduction and drag it into the mountain. Edit the look of the mountain further by editing the Reduction Strength.
Step 8
Repeat the same steps to create a few more mountains and arrange them to your liking. Change the Seed in the options menu to change the look of the mountain.
Step 9
Ensure you have the mountain Landscape selected. By clicking up or down on the Seed option you can scroll through different randomised mountain shapes. Select a number that you like.
Remember you can always go back through all the different options that you used—Polygon Reduction, Displacer and Random—to further tweak the island and landscapes to the shapes that you like.
5. How to Create Mini Trees
Step 1
To create the trees for our floating island, go back to the blue Add Cube Object button again. Primary-Click-Hold and then select the Pyramid tool from the menu.
Step 2
With the Pyramid selected, click on the Make Editable button and use the Scale Tool to resize it and reshape it to the appropriate size.
Step 3
Select the Points Tool on the left side of the screen and then use the Move Tool (E) to move the top of the Pyramid down a little. This squashes the object a little bit so that I can fit a second Pyramid underneath.
If you prefer the look of a taller tree, however, you can leave it as it is or even move the point further up.
Step 4
Next, you will want to duplicate the pyramid. Control-Click-Drag on your object on the right of the screen. Then using the Move Tool (E) place one pyramid on top of the other.
Step 5
For the tree trunk, we'll need to create a cube. Click on the Cube button and following the same steps as before scale it down and position it below the two pyramids to create a tree.
Group all the tree objects together by selecting them all and pressing Alt-G. Before duplicating the group and adding it to the island, I'll move on to the next step where we will learn how to add colour.
6. How to Add Colour to 3D Objects
Step 1
To add some colour to our 3D objects, click on the Create button at the bottom left of the screen. Then select New Material.
Step 2
Double click on the small sphere icon that appears. In the material editor, you have access to a variety of options that will change the appearance and properties of the shapes that you create.
Untick Reflectance.
Step 3
Click on Color. Choose the colour you want—you can see the colour I have used for the tree below. Once you've chosen the colour, close the material editor.
Step 4
Click-Drag the material onto your tree to apply the material colour.
Step 5
Repeat the steps for the tree trunk and rest of the floating island.
7. How to Group and Duplicate you Objects
Step 1
I'll begin to populate out floating island with the trees that I created earlier. I must first select all the objects associated with the tree (cube, pyramid x2) and then press Alt-G on the keyboard to create a group.
Rename the group from Null to Tree and duplicate the group to create more trees.
Step 2
Hold the Control key on the keyboard while you Click and Drag the tree group to the section below. This duplicates the whole group.
Do this several times until you have the desired number of trees for the island.
Step 3
Use the Move Tool (E) to move the trees around the island.
8. How to Create the Moon
Step 1
To create the moon for the floating island, return to the blue Add Cube Object button again. Left Click-Hold and then select the Sphere tool from the menu.
Step 2
Delete the Phong Tag. I'll add a Polygon Reduction and a Displacer to the Sphere. Edit the settings to your liking as before.
Step 3
Add a colour to the moon and use the Move Tool (E) to the location that you like.
9. How to add Lighting and Render the Scene
Step 1
Choose a suitable angle in the viewport by navigating around the scene. Click on the Camera button at the top to create a camera.
Step 2
To set up the basic lighting, you’ll want to go to the Floor button located in the top menu bar. Left Click-Hold and then select Physical Sky from the menu.
Step 3
Ensuring that Physical Sky is selected in the menu on the right, a new menu will appear on the bottom right of the screen. Select the Time and Location tab and choose a time using the left and right arrow keys. This changes the lighting.
Step 4
Click on the Render Settings button on the top menu bar.
Step 5
Choose the resolution, height and width of your image. For this tutorial I have chosen 1920x1200 72dpi.
Step 6
Choose where you'd like to save your file and the file name. You may wish to tick Alpha Channel on, if you want to continue editing the image in another program like Adobe Photoshop.
Step 7
Go to the Effect button at the bottom left and select both Ambient Occlusion and Global Illumination from the drop down menu. This will add these options to the render.
Step 8
Click the Render button and wait for your render to finish!
Conclusion
And with that, the 3D Floating Island is complete. Feel free to share your own creations below.
You can also export the image into Adobe Photoshop to enhance it further or to use it as part of a larger illustration.
For players, one of the most scrutinized aspects of a video game is its immersion factor. For designers, it’s creating the illusion with seamless finesse. From the first apprehensive steps of level 1 to the final bold strut of level 70, players are deeply woven into an economic system.
How much they notice, however, is the measure of its designers’ success. Especially evident in Massively Multiplayer Online Role Playing Games (MMORPGs), closed-economy structures are magnificent to behold. These games are composed of potentially thousands of tightly controlled interlocking inputs and outputs, and ostensibly appear to be purely about their chosen theme.
Behind the scenes exists a network of intricate maths and economic principles continuing to maintain that immersion. To illustrate, players fresh out of character creation will inevitably begin by doing the following: exploring the environment, conquering enemies, and then purchasing items. This progression will also often occur in that same order. It’s predictable for a reason, and this article will explore why. The concepts explored below will explain how closed economies operate and sustain themselves and the challenges throughout.
Basics of Stability
When conceptualizing an economy, game designers must begin by choosing a currency. Depending on thematic overtures, the name of the currency will vary. For example, many games use the prototypical “gold” moniker. Whether the setting takes place in a fantastical realm or a futuristic timescape will heavily influence the type of currency depicted.
In the case of a closed economy, currency carries a set value. Some designers utilize open economies, however, which operate similarly to real-world money. There are exchange rates, and differential values, which are each relative to one another’s values. Using this model significantly increases the difficulty of creating and maintaining the currency within the game’s world but allows a realistic perspective and some compelling implications. For the purpose of this article, the focus will be on the former.
For example, if an iron sword costs 100 gold, it is expected that it will continue to cost 100 gold. This basic stability allows for the remaining structure to be upheld throughout each play through. While in-game currency, gold, is what your players will be trading, the actual currency that an MMO employs is time. Operating as the game’s independent variable, player progression serves as a substitution for time passing. This commonality between games allows balance for the economy since items can easily have pre-determined time values.
A few basic principles can govern this process nicely. For example, the less progress a player experiences (often expressed as “XP”), the less expensive the product remains. The inverse is also true. So product price and player progression have a directly corresponding relationship.
At level 1, the local tavern sells an iron sword at 100 gold. The difficulty of an event and the reward given share a directly corresponding relationship as well. So a player defeats a worm at level one and receives 5 gold. A player can purchase this sword at any time, but will most likely buy it very early in the game because there is less money available and the item is weaker. It is best suited for the level 1 warrior and will predictably be purchased by the level 1 warrior. Mechanisms like these control the hidden economic fabric of the traditional MMO.
Keeping these principles inextricably connected will continue to provide economic stability to any MMO. Introducing unbalanced items, like downloadable content weaponry that is significantly stronger than currently available weapons, will always disrupt the network. To counterbalance, these items are often not worth much in-game currency or cannot be used until the player reaches a certain level. When these measures are not taken, however, players may feel less engrossed in the game.
Taking this out of theory and into brass tacks, let’s continue.
System Inputs
Developing the specific input modality for item creation and currency allotment is less complicated than it sounds. To begin, there are three interchangeable models for how to complete this feat: utilizing generators, creating event-based inputs, and compiling individually matched inputs.
Generators can be used for anything that regularly adds to the economy without necessary interaction, the only stipulation being some resource-dependency. For example, if you have a player strike oil during renovations, the amount of oil can be determined by the generator, but whether the player uses the pickaxe is his or her prerogative. So when mining for resources, if an unusually specific amount of coal appears on the player’s screen, this is the culprit. After completing the unrelated task the player was engaged in prior to the generator’s activation, he or she may check the inventory and voila! That resource will have manifested itself.
Event-based inputs can be used when given conditions are met. This is most often used in recurring collection quests—quests that require extracting certain herbs from plants to concoct a potion, for example. The formula behind the quest is: “x plant harvested = y herb extracted”. This modality is often tapped for crafting quests and side-missions.
Single inputs are used for circumstantially driven resource acquisition. When certain conditions are met, but they are also not repeatable, these items are offered—for example, when a player conquers a specific opponent and it drops a unique helmet. These are usually reserved for boss battles and story-driven events since they do not require repetition.
System Outputs
Functioning in the opposite way, system outputs remove items and currency from your economy. Similarly to system inputs, there are three major categories: degenerators, event-based outputs, and single outputs.
Degenerators regularly take away resources that do not necessitate interaction. While more sparingly used than their input counterpart, these appear as ongoing costs throughout game play—for example, tax collection that briefly pops up on the Heads Up Display at regular intervals. This oddly specific number will turn up on its own accord without player interference. When checking the inventory, the player will suddenly be missing exactly that amount of currency.
Event-based outputs are used when players complete specific actions that remove resources from their inventories. For instance, a player sells that iron sword to the shopkeep. In doing so, the event-based output activates to remove the sword from the player’s inventory. Depleting resources while crafting, submitting items for quests, and dropping unwanted items all constitute event-based outputs.
Single outputs are utilized when a resource is removed from the economy altogether and cannot be altered again. Many designers use this mechanism when items must be delivered or destroyed based upon story elements—for example, when obtaining a quest item that players cannot use. These items are usually distinguishable from the others by being held in a separate section of the player’s inventory—like “key items”—and can only be removed when certain events unfold.
Artificial Stability in an Unstable World
Utilizing all three techniques enables game designers to create seamless worlds for their players—if they are in harmony. Ideally, the planned economy would be a perpetually operative and well-oiled machine. It would have a set of inputs that exactly equals the number of outputs, which together produce a purely immersive experience.
However, the ideal rarely becomes real. Being human hosts a number of spectacular pros, but one con is that we are not perfect. We are not machines, and thus our creations will usually come to fruition with flaws. To keep these flaws at a minimum, and relatively minuscule, this article will leave you with some machine-ly wisdom from experience.
Flag Impactful Items: to most efficiently contain simple mistakes in an intricate economy, assign priorities to each resource based on its orb of influence. For example, implementing tighter controls on an epic-level sniper rifle should become a higher priority since it can easily upset the economy. Having an overabundance of iron ore, however, is less likely to have such a significant effect. This way, when entering miles of code in the vast matrix that is the economy, little red flags will tip off any developer that this particular item is a heavy-hitter.
Don’t Underestimate Bug Checkers: having players test an alpha version of any game is the first line of defense against potentially disastrous economies. Though some issues may not become evident until more time has passed, these players are ready and willing to try. If an enemy is just too powerful or a weapon is just too rare, they will be upfront with their critique. If this prospect seems a little too gut-wrenching, having friends and loved ones look it over is always a gentler option.
Matrix Double-Glance: daunting, yet unfailingly practical, this technique involves compiling all the game’s items included in the economy and listing them in spreadsheets. Check it against itself forwards, and backwards, and then code it manually. To add another level of security, implementing an automated system and letting it convert the items will drastically improve both accuracy and speed.
Abandoned Accounts: multiplayer economies have several unique challenges to keep in mind. Suspended, dormant, and otherwise abandoned accounts can create unaccounted-for outputs across a wide variety of resources. This situation can be combated by building slightly different expected-resources-per-player ratios. Do away with generators and de-generators that operate at a fixed rate. Instead, control the flow of resources based on the current amount operating in the economy. This should eliminate any wealth hoarded by abandoned accounts.
Conclusion
Creating a multiplayer economy is an ambitious but rewarding journey. Being able to observe the careful cogs of code manifest themselves as a bustling marketplace is truly spectacular. Even when players gripe about how expensive the rare daggers are, designers know that it is all working according to plan.
This article has only scratched the surface of what makes MMORPG economies thrive. But the first step is always the hardest! Being able to successfully track resources is a fundamental building block that will launch any game developer ahead in his or her project and field. Examples of how the next steps in this journey may look include exploring ideal resource ratios, establishing player-to-player commodity trading, virtual banks for storing currency, and many more.
To continue the discussion, feel free to drop a comment in the box below!
Last month, we made a big announcement: unlimited stock photos on Envato Elements. Now, to celebrate the launch, we're giving away 16 free images and design assets, to give a small taste of what Elements has to offer. They all come with a simple license allowing you to use them in any single project you want.
The Freebies You Can Download
So what exactly can you download? You'll find a selection of beautiful photos, such as this stunning underwater shot:
You'll also find striking portraits, creative close-up shots, landscape images, and work photos suitable for illustrating a report or proposal:
And it's not just images you can download. There's also an amazing Photoshop action that allows you simply to brush onto areas of a photo and play the action to turn those areas into a powerful sandstorm effect.
And on top of all this, you can download an elegant script font, a flyer template, and a PowerPoint presentation template.
How to Unlock Your Free Photos
To access the free photos and design assets, all you have to do is go to the launch page and enter your email address. Then you can download as many of the free files as you want.
If you like what you see, you can also explore the full range of more than 200,000 stock photos now available on Envato Elements, or browse the thousands of web templates, 3D renders, graphics and more that come with a monthly subscription. But if it's not right for you, no worries—you can just enjoy your freebies, with no obligation to buy anything.
This tutorial is going to cover how to implement a basic augmented reality application with Vuforia SDK on Unity. Throughout the tutorial, you are going to learn how to generate a scene compatible with Vuforia and how to implement various scripts, including ray-tracing, in order to be able to interact with the object within the scene.
Creating a Developer Account
Before starting off, you need to register for a Vuforia developer account. Go to the Vuforia Developer Portal to create an account.
Once you create your account, it is time to download the Unity package of Vuforia. Go to the download page and download the specific package for Unity.
Creating an Image Target
An image target is required in order for a device's camera to recognize a reference and track it. The orientation and actual size of the target image directly affect the same attributes of the superimposed images.
Any image can be assigned as a target. However, the features of the target image effectively determine how well the target is tracked. In this tutorial, we are going to use an online tool to generate feature-rich target images. Generate an image target by using the Augmented Reality Marker Generator online tool, and save the image on your computer.
Preparing the Unity Scene
Vuforia Package
Create a new 3D unity project and then double-click the Unity package you downloaded for Vuforia. This will prompt the following window. Click All to select all the content of the package, and then hit Import. This is going to import all the necessary tools for the AR application.
Image Target
The next step is to import the image target files. In order to obtain the image target files, we need to use the Vuforia developer page.
Go to the Vuforia Developer Portal and then log in to your account. Under the Develop tab, you will see the Target Manager. First you need to add a database. Use the designated button and add a database.
Name your database as you wish, and select Device as the type.
Now we are ready to add a target in this database. Click on the Add Target button, and the following window will appear. The type should be selected as Single Image in our case. Select the image target that we generated by using the online tool. If you have any trouble uploading the file, try converting it to .jpg file format and uploading again.
Width is a crucial parameter. This should match the real size of the target image that you will be eventually printing on paper. I set the width to 40. There is no unit since it matches the unit of your scene.
Once you add the target into your database, Vuforia rates your target. With the target image generator we used, features are high and therefore it gets 5 stars, which means it's easy for Vuforia to recognize and track this target.
Now you need to download this database. To do so, hit the Download Database button and select Unity Editor as the development platform.
Once you've downloaded the database, double click on it and import all the content to the Unity scene we are working on.
ARCamera Object
We start by adding the ARCamera object of Vuforia to our scene. To do so, simply follow the Assets > Vuforia > Prefabs directory and add the ARCamera object by dragging and dropping to the scene.
Select the ARCamera object and under the Inspector tab, you will see the App License Key section. This license key will be obtained from the Vuforia developer portal.
Log in to your Vuforia account on the Developer Portal and under the Develop tab, you will find the License Manager section. Click the Add License Key button. On the following page, select Development as the project type and define an application name for your project. This name is not crucial, and you can alter it later on if you wish.
Hit Next, and then confirm your license key on the next page.
Select the license you've just created. It will reveal the license key that we need to copy and paste to the ARCamera object. Copy this license and then paste it into the App License Key section under the ARCamera settings.
Under Database Load Behaviour, check the Load ARdemo Database option. Once you check it, another option called Activate will appear right under it. Check this option as well.
The ARdemo part of the Load ARdemo Database option depends on how you named your database.
Image Target Object
The second object we need in our scene is the Image Target object.
Under the Assets > Vuforia > Prefabs directory, you will also find the "ImageTarget" object. Add this object to your scene and select it to reveal the options.
Under the Image Target Behaviour section, you will see the Database option. Select your database from the dropdown menu and select the specific image target you want to assign to the image target object from the "Image Target" option's dropdown menu. If you have multiple image targets for one database, they will all be listed here.
The width and height parameters will be automatically set depending on the value you assigned when creating the image targets in Vuforia's developer portal.
Augmenting Graphics
The next step is to create the graphics and tie them to the image target. You can either create a GameObject or you can import your own 3D model into Unity and use it. In this tutorial we are going to use a simple 3D cube object for the sake of simplicity.
Add a cube object to the scene as shown in the following figure.
Set its x, y and z parameters for the Scale option under Transform to 40, so that it matches the size of the image target we generated.
If you set another width value for your image target when generating it in the developer portal, use the value you selected in order to match the full size of the image target.
The last step to get our AR app working is to set the cube object as the child of the image target. To do so, simply drag the cube object and drop it on the imageTarget object under the hierarchy menu.
The final state of the scene should be as follows:
Now hit the Play button to run your application. It will use your webcam. Either get the target image printed or open it from your phone so that Vuforia can detect it through your webcam. I did the latter and opened the target image from my phone.
Here is the actual screenshot of the view of the webcam. You can see that the cube object covers the whole target image, since we matched the scaling factor values both for the 3D object and the target image.
Interaction Scripts
So far, we've developed a basic AR application that recognizes and tracks our target image and displays the designated 3D graphics. However, for a complete AR application, we also need to be able to interact with the objects, augmenting the reality.
For this purpose, we need to be able to detect where we clicked—or touched, in the case of a mobile device. We'll do this by implementing a ray-tracer.
First, create a folder named "scripts" under Assets to keep everything organized. We are going to store our script files in this folder. Then create a C# Script file in this folder. Name it "rayTracer". Naming is important due to the fact that the following code should match this specific file name. If you prefer to use a different name for your script file, you should also change the provided code accordingly.
Ray-Tracer Script
Copy and paste the following code into the C# Script file you have just created and named "rayTracer".
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class rayTracer : MonoBehaviour {
private List<GameObject> touchList = new List<GameObject>();
private GameObject[] touchPrev;
private RaycastHit hit;
void Update () {
#if UNITY_EDITOR
if (Input.GetMouseButton(0) || Input.GetMouseButtonDown(0) || Input.GetMouseButtonUp(0)) {
touchPrev = new GameObject[touchList.Count];
touchList.CopyTo (touchPrev);
touchList.Clear ();
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
//Debug.DrawRay(ray.origin, ray.direction*10000, Color.green, 10, false);
if (Physics.Raycast (ray, out hit)) {
GameObject recipient = hit.transform.gameObject;
touchList.Add (recipient);
if (Input.GetMouseButtonDown(0)) {
recipient.SendMessage ("touchBegan", hit.point, SendMessageOptions.DontRequireReceiver);
}
if (Input.GetMouseButtonUp(0)) {
recipient.SendMessage ("touchEnded", hit.point, SendMessageOptions.DontRequireReceiver);
}
if (Input.GetMouseButton(0)) {
recipient.SendMessage ("touchStay", hit.point, SendMessageOptions.DontRequireReceiver);
}
}
foreach (GameObject g in touchPrev) {
if(!touchList.Contains(g)){
g.SendMessage ("touchExit", hit.point, SendMessageOptions.DontRequireReceiver);
}
}
}
#endif
if (Input.touchCount > 0) {
touchPrev = new GameObject[touchList.Count];
touchList.CopyTo (touchPrev);
touchList.Clear ();
foreach (Touch touch in Input.touches) {
Ray ray = Camera.main.ScreenPointToRay (touch.position);
if (Physics.Raycast (ray, out hit)) {
GameObject recipient = hit.transform.gameObject;
touchList.Add (recipient);
if (touch.phase == TouchPhase.Began) {
recipient.SendMessage ("touchBegan", hit.point, SendMessageOptions.DontRequireReceiver);
}
if (touch.phase == TouchPhase.Ended) {
recipient.SendMessage ("touchEnded", hit.point, SendMessageOptions.DontRequireReceiver);
}
if (touch.phase == TouchPhase.Stationary || touch.phase == TouchPhase.Moved) {
recipient.SendMessage ("touchStay", hit.point, SendMessageOptions.DontRequireReceiver);
}
if (touch.phase == TouchPhase.Canceled) {
recipient.SendMessage ("touchExit", hit.point, SendMessageOptions.DontRequireReceiver);
}
}
}
foreach (GameObject g in touchPrev) {
if(!touchList.Contains(g)){
g.SendMessage ("touchExit", hit.point, SendMessageOptions.DontRequireReceiver);
}
}
}
}
}
This script detects both mouse clicks if you are working on the Unity editor and touch inputs if you have deployed your application on a mobile device with a touch screen.
Once you've created your rayTracer script, you need to activate it by assigning it to one of the objects in the scene. I selected the ARCamera object and added the rayTracer scripts as a component by using the Add Component button under the Inspector tab.
Object Material
Now we are going to assign a material to our Cube object and change the color of the material upon interaction with the cube.
Under Assets, create a material and name it as you wish.
Now assign this material by dragging and dropping over the cube object.
Interaction Script
Create a new C# Script under the scripts folder and name it "interaction".
Copy the following C# code into your "interaction" script file and then add this script file to the cube object as a component, just as we did with the "rayTracer" script file. However, this time it should be a component of the cube object—this is important in order to be able to only interact with the cube object.
using UnityEngine;
using System.Collections;
public class interaction : MonoBehaviour {
public static Color defaultColor;
public static Color selectedColor;
public static Material mat;
void Start(){
mat = GetComponent<Renderer> ().material;
mat.SetFloat("_Mode", 2);
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
mat.SetInt("_ZWrite", 0);
mat.DisableKeyword("_ALPHATEST_ON");
mat.EnableKeyword("_ALPHABLEND_ON");
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
mat.renderQueue = 3000;
defaultColor = new Color32 (255, 255, 255, 255);
selectedColor = new Color32 (255, 0, 0, 255);
mat.color = defaultColor;
}
void touchBegan(){
mat.color = selectedColor;
}
void touchEnded(){
mat.color = defaultColor;
}
void touchStay(){
mat.color = selectedColor;
}
void touchExit(){
mat.color = defaultColor;
}
}
In this "interaction" script, we are referring to the material of the cube object as "mat".
We created two different material objects named defaultColor and selectedColor. defaultColor is selected to be white, as the RGBA parameters indicate, which are (255, 255, 255, 255).
We initialize the cube object's material color as defaultColor by the following line:
mat.color = defaultColor;
We have four different functions for four different states:
touchBegan() is called at the instant you touched on the object.
touchEnded() is called when you release your finger.
touchStay() is called right after you touched on the object—this function follows touchBegan(). So, if you assign different colors to your material in these functions, you are unlikely to see the color assigned in the touchStay() function, since it is the very first instant the touch is recognized.
touchExit() is called when you drag your finger out of the cube object's surface, instead of releasing your finger, which calls the touchEnded() function as explained above.
In our code, when we touch on the cube object, we assign the selectedColor object to mat.color, which is the color of our cube object's material.
By assigning the selectedColor within the touchStay() function, we make sure that the color of the cube object will be equal to selectedColor as long as we keep our finger on the cube object. If we release our finger or drag it out of the cube object, we assign defaultColor to the material's color parameter by calling the touchEnded() or touchExit() functions in accordance with the action we took.
Now run the project and click on the cube object once the target image is recognized and the cube object has appeared. It should turn red and white again when you release your click or move it out of the cube object's surface.
You can experiment with different colors for the four different actions to comprehend them thoroughly.
Conclusion
In this tutorial, we've gone through an introduction to the Vuforia SDK for Unity along with its developer portal, and we've seen how to generate a target image and an appropriate license key.
On top of that, we generated custom script files in order to be able to interact with the augmented graphics. This tutorial is just an introduction to enable you to start using Vuforia with Unity and creating your own AR applications.
In this post we'll build a simple game from scratch. Along the way, we'll touch on some of the most important aspects of the SpriteKit library.
This post builds on what we've learned earlier in the SpriteKit Basics series. If you want to refresh your SpriteKit knowledge, take a look at some of my other posts.
Open Xcode and start a new project from the menu File> New > Project. Make sure iOS is selected and choose Game as your template.
Give your project a name, and make sure that Language is set to Swift,Game Technology is set to SpriteKit, and Devices is set to iPad.
Planning the Game Scenes
One of the first things I like to do when creating a project is to determine how many scenes I will need for the project. I will usually have at least three scenes: an intro scene, a main game scene, and a scene to show high scores, etc.
For this example, we just need an intro and main gameplay scene since we won't be keeping track of lives, scores, etc. SpriteKit already comes with one scene when you create a new project, so we just need an intro scene.
From Xcode's menu, choose File> New> File. Make sure iOS is selected, and choose Cocoa Touch Class.
Name the class StartGameScene, and make sure that Subclass of is set to SKScene and Language is set to Swift.
Setting Up GameViewController
Open GameViewController.swift. Delete everything in that file and replace it with the following.
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = StartGameScene(size: view.bounds.size)
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = false
scene.scaleMode = .aspectFill
skView.presentScene(scene)
}
override var prefersStatusBarHidden: Bool {
return true
}
}
When you create a new project, GameViewController.swift is set up to load GameScene.sks from disk. GameScene.sks is used along with SpriteKit's built-in scene editor, which allows you to visually lay out your projects. We will not be using GameScene.sks, and will instead create everything from code, so here we initiate a new instance of StartGameScene and present it.
Create the Intro Scene
Add the following to the newly created StartGameScene.swift.
import UIKit
import SpriteKit
class StartGameScene: SKScene {
override func didMove(to view: SKView){
scene?.backgroundColor = .blue
let logo = SKSpriteNode(imageNamed: "bigplane")
logo.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(logo)
let newGameBtn = SKSpriteNode(imageNamed: "newgamebutton")
newGameBtn.position = CGPoint(x: size.width/2, y: size.height/2 - 350)
newGameBtn.name = "newgame"
addChild(newGameBtn)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if(touchedNode.name == "newgame"){
let newScene = GameScene(size: size)
newScene.scaleMode = scaleMode
view?.presentScene(newScene)
}
}}
This scene is pretty simple. In the didMove method, we add a logo and a button. Then, in touchesBegan, we detect touches on the new game button and respond by loading the main scene GameScene.
Planning Game Classes
The next thing I like to do when creating a new game is decide which classes I will need. I can tell right away that I will need a Player class and an Enemy class. Both of these classes will extend SKSpriteNode. I think for this project we will just create the player and enemy bullets right from within their respective classes. You could make separate player bullet and enemy bullet classes if you prefer, and I suggest you try to do that as an exercise on your own.
Lastly, there are the islands. These do not have any specific functionality but to move down the screen. In this case, since they're just decorations, I think it's also okay not to create a class, and instead just create them in the main GameScene.
Creating the Player Class
From Xcode's menu, choose File> New> File. Make sure iOS is selected and choose Cocoa Touch Class.
Make sure that Class is set to Player, Subclass of: is set to SKSpriteNode, and Language is set to Swift.
Now add the following to Player.swift.
import UIKit
import SpriteKit
class Player: SKSpriteNode {
private var canFire = true
private var invincible = false
private var lives:Int = 3 {
didSet {
if(lives < 0){
kill()
}else{
respawn()
}
}
}
init() {
let texture = SKTexture(imageNamed: "player")
super.init(texture: texture, color: .clear, size: texture.size())
self.physicsBody = SKPhysicsBody(texture: self.texture!,size:self.size)
self.physicsBody?.isDynamic = true
self.physicsBody?.categoryBitMask = PhysicsCategories.Player
self.physicsBody?.contactTestBitMask = PhysicsCategories.Enemy | PhysicsCategories.EnemyBullet
self.physicsBody?.collisionBitMask = PhysicsCategories.EdgeBody
self.physicsBody?.allowsRotation = false
generateBullets()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func die (){
if(invincible == false){
lives -= 1
}
}
func kill(){
let newScene = StartGameScene(size: self.scene!.size)
newScene.scaleMode = self.scene!.scaleMode
let doorsClose = SKTransition.doorsCloseVertical(withDuration: 2.0)
self.scene!.view?.presentScene(newScene, transition: doorsClose)
}
func respawn(){
invincible = true
let fadeOutAction = SKAction.fadeOut(withDuration: 0.4)
let fadeInAction = SKAction.fadeIn(withDuration: 0.4)
let fadeOutIn = SKAction.sequence([fadeOutAction,fadeInAction])
let fadeOutInAction = SKAction.repeat(fadeOutIn, count: 5)
let setInvicibleFalse = SKAction.run {
self.invincible = false
}
run(SKAction.sequence([fadeOutInAction,setInvicibleFalse]))
}
func generateBullets(){
let fireBulletAction = SKAction.run{ [weak self] in
self?.fireBullet()
}
let waitToFire = SKAction.wait(forDuration: 0.8)
let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
let fire = SKAction.repeatForever(fireBulletSequence)
run(fire)
}
func fireBullet(){
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.position.x = self.position.x
bullet.position.y = self.position.y + self.size.height/2
bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
bullet.physicsBody?.categoryBitMask = PhysicsCategories.PlayerBullet
bullet.physicsBody?.allowsRotation = false
scene?.addChild(bullet)
let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y:(scene?.size.height)! + bullet.size.height), duration: 1.0)
let removeBulletAction = SKAction.removeFromParent()
bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction]))
}
}
Within the init() method, we set up the physicsBody and invoke generateBullets(). The generateBullets method repeatedly calls fireBullet(), which creates a bullet, sets its physicsBody, and moves it down the screen.
When the player loses a life, the respawn() method is invoked. Within the respawn method, we fade the plane in and out five times, during which time the player will be invincible. One the player has exhausted all the lives, the kill() method is invoked. The kill method simply loads the StartGameScene.
Creating the Enemy Class
Choose File> New> File from Xcode's menu. Make sure iOS is selected and choose Cocoa Touch Class.
Make sure that Class is set to Enemy, Subclass of: is set to SKSpriteNode, and Language is set to Swift.
This class is pretty similar to the Player class. We set its physicsBody and invoke generateBullets(). The move() simply moves the enemy down the screen.
Creating the Main Game Scene
Delete everything within GameScene.swift and add the following.
We create an instance of Player and an instance of CMMotionManager. We are using the accelerometer to move the player in this game.
Within the didMove(to:) method we turn off the gravity, set up the contactDelegate, add an edge loop, and set the player's position before adding it to the scene. We then invoke setupAccelerometer(), which sets up the accelerometer, and invoke the addEnemies() and generateIslands() methods.
The addEnemies() method repeatedly calls the generateEnemy() method, which will create an instance of Enemy and add it to the scene.
The generateIslands() method works similarly to the addEnemies() method in that it repeatedly calls createIsland() which creates an SKSpriteNode and adds it to the scene. Within createIsland(), we also create an SKAction that moves the island down the scene.
Within the didBegin(_:) method, we check to see which nodes are making contact and respond by removing the appropriate node from the scene and invoking player.die() if necessary. The createExplosion() method creates an explosion animation and adds it to the scene. Once the explosion is finished, it is removed from the scene.
Conclusion
During this series, we learned some of the most important concepts used in almost all SpriteKit games. We ended the series by showing how simple it is to get a basic game up and running. There are still some improvements that could be made, like a HUB, high scores, and sounds (I included a couple of MP3s you can use for this in the repo). I hope you learned something useful throughout this series, and thanks for reading!
If you want to learn more about game programming with SpriteKit, check out one of our comprehensive video courses! You'll learn how to build a SpriteKit game from A to Z.
Pixel art is often viewed as just a retro style of graphics made for the purpose of nostalgia; a throwback to the early days of video games. However to me, and many other lovers of pixel art, it’s the precision, intricacy and aesthetic born of meticulously placing each pixel onto a miniature stage that truly makes it a wonderful and fascinating art form–one that belongs just as much today as it did yesteryear. Outstanding pixel artists often remind me of artisans who create tiny ships in tiny bottles; achieving a lot with a little is always impressive.
Just as pixel art is an exacting art form, so too can it be helped along by equally exacting applications. Adobe Photoshop has been a common application of choice for pixel artists for a long time, but there exist today some fantastic alternatives, and those designed expressly for pixel art arguably offer the most.
In this article we’ll be looking at five alternative applications for pixel art, and the tools they include that can help pixel artists along. Some of the key types of tools we’ll be looking for are: selecting and resizing tools that don’t add anti-aliasing, access to pixel art color palettes, pixel friendly shortcut tools for things like gradient creation and dithering, help with tilemap creation and animation, easily accessible color selectors for quick changes while drawing, pixel friendly shape drawing tools, real size preview pane, among others.
As far as deciding which software and feature set is “best” for pixel art, it goes a little further than just confirming the inclusion of the most commonly sought after tools, because there are roughly three subsets of pixel art creation to consider:
Seamless tiles for games
Sprites & animation
Complete artworks
Whether an application is suitable to you can depend on which of the above types of pixel art you do. As we go through these alternatives to Photoshop for pixel art I’ll try to fill you in on which applications have features well suited to each area.
The beauty of dedicated pixel art applications is they tend to be very
inexpensive, so you don’t even really have to choose a single application you want to use. You could get every application on this list
and still have only spent $23.99, coming in well under our series per application price cap of $100.
Let’s start taking a look at what we have to play with!
1. Pyxel Edit
First cab off the rank is Pyxel Edit. I’m talking about this program at the top of our list, despite its current beta status, because it has strong tools for all three of the pixel art areas we described in the introduction to this article–something quite rare.
It includes absolutely fantastic tile tools, (not only for
seamless tile creation but also full tilesets and even exportable tilemaps), it has animation tools that give you all the essentials, and it allows you to create complete artworks as well. This is software built from the ground up with pixel artists, and in particular game artists, in front of mind.
Pyxel Edit has a lot of great features, but to me its tile creation tools are the real standout, so if you’re involved in game development this might be of particular interest to you. You can define tiles as you go
along and place multiple instances of them anywhere in your document. Then if you paint on one instance of a tile all other instances are updated in real time. This is very helpful for creating seamless textures as you can place tile instances next to each other and paint right over the seams.
Instead of painting over the seams between tiles, you also have the option to lock your pencil to only draw within the constraints of the tile you started on. And with the offset tool you can click & drag to shift the contents of an entire tile, with the displaced pixels wrapping around to reappear on the opposite side of the tile.
A big benefit of Pyxel Edit’s excellent tile system is you can see how
your tiles will actually look together in a real level, instead of working on your game art in a vacuum. You can watch this process in action in the video below.
Pyxel Edit’s tile functionality means you can also use it as your main level design
tool if you choose. It has the ability to export
your tilemap data in JSON, XML or TXT format so, depending on the game
engine and toolset you use, you may be able to load this data directly into your game projects. For example, you can use this importer addon for Unity, Haxe has an importer, and I understand Phaser has Pyxel Edit importing baked in.
As well as the great tile editing tools, Pyxel Edit checks almost all the boxes for the most commonly required tools for pixel art. It provides a 100% scale preview in the sidebar, it has a “clean 1 pixel line” setting for drawing lines you don’t have to painstakingly tweak afterwards, it has rectangle and ellipse tools, easy color picker access with right-click or ALT+left-click, great color palette management with classic palettes preloaded, and very good selection tools. It also autosaves your work as you go along.
Duplication
and movement of pixels is made very easy in Pyxel Edit with a handy set of shortcuts.
Hit S to select an area, CTRL to move a selection of pixels, or CTRL+ALT to duplicate and move those pixels. When working with tiles you can also double-click any tile with the Select tool active to select the whole tile, making it easy to duplicate frames for adjustment.
Unfortunately, however, the software doesn’t seem to have live mirroring, (though you can flip selections horizontally or vertically), or any type of dithering tools at present. Hopefully we’ll see these features added as the project moves through its beta phase.
Another really standout set of features in Pyxel Edit is its color management tools, in particular its automated suggestions of different colors, which are generated by adjusting your current color’s shading, lightness, saturation and hue:
These
color suggestion tools are so useful I wish they were in every
graphics application. Just by selecting a good “seed” color right at the beginning of
your project you can get a glimpse of how your entire color palette
might look.
Another great tool in Pyxel Edit is its Color Replace tool, which allows you to easily change your mind about colors after you’ve put them on the canvas. It will replace any pixels of the current secondary color, (equivalent to Photoshop’s background color), with the current primary color, (equivalent to Photoshop’s foreground color). So you just sample the color you want to change into the secondary slot, choose your new color in the primary slot, and draw over the pixels whose color you want to alter.
You also get some help with the creation of gradients: choose two colors in your palette and Pyxel Edit can generate a selection
of graduating colors in between them. With these colors you can then paint out your gradients.
I’ve focused primarily here on Pyxel Edit’s tile and drawing tools, but it also includes solid animation tools. The layout for animation is the same as for tile creation, whereby you work in a grid layout and place a frame of your animation into each one of the grid squares. You can create multiple animations within a single document, and you can control the time each frame in your animation is held. You can also turn on onion skinning, however it can only be used to show
the frame before and after any particular frame.
And finally, if you don’t want to create either a tileset or animation, rather you want to create a single artwork, you just select the “Single image document” option when creating a new file.
Pyxel Edit is a great tool, especially for people involved in game dev, and it can be picked up for $9 while it’s still in beta. It runs on Adobe Air so natively it only works on Mac and Windows, however I’ve found it runs quite well on Linux via Wine also.
Platforms: Mac, Windows, (also works via Wine on Linux)
Price: $9 (Currently in beta)
2. Aseprite
Aseprite is the favorite tool of a great many pixel artists, and with its outstanding range of pixel art specific tools, that’s for good reason. Everything about Aseprite is 100% focused on pixels–even the UI itself is done in pixel art style.
Where Aseprite shines is as a tool for sprite creation and animation. It has a powerful layer and frame based animation system with several features that are not present in many pixel art focused applications.
There are many animation features in Aseprite, too many to list here, but I’ll give you a quick rundown. You have the ability to move multiple frames in your sequence to a new spot in the timeline, copy and paste a number of frames at a time, set onion skinning to show as many of your previous and next frames as you choose, loop or reverse sections of your timeline, move and copy frames (all layers) or cells (selected layers within a frame), and control frame duration. You can read about the full list of animation functions in Aseprite’s animation docs.
As well as the animation features, all the most essential tools for pixel art are present in Aseprite. You have a 100% preview window that floats over your main window, a “pixel perfect” mode for drawing clean lines, great color palette management with the ability to drag & drop swatches to reorder them, easy color picking by holding ALT then left mouse clicking, filled and empty ellipse tools, filled and empty rectangle tools, and a series of really fantastic selection tools including rectangle and ellipse marquees, magic wand, lasso tool and polygonal lasso tool.
On top of that it has quite a long list of super useful drawing tools. For example, with the Contour Tool you can draw an arc or partial outline and it will automatically be closed and filled for you. With the Polygon Tool you can draw out shapes with as many straight sides as you want and it will be automatically filled. With the Spray Tool you can add randomized speckles.
With the Curve Tool you can draw pixel perfect curved lines with a method comparable to using bezier curves. With the Jumble Tool you can randomize the location of an area of pixels to rearrange them. And with the Blur Tool you can generate pixels around the edge of a shape that blur it with the background color.
While drawing you can activate either horizontal or vertical symmetry modes, great for things like character faces. However you’re not stuck with said symmetry being around the center of your document. With either mode activated a line appears on your canvas and you can shift it to any position and draw your symmetrical shape there.
One
of Aseprite’s coolest features, in my view, is the “Shading” ink mode which allows you to
select a series of colors and use them all to automatically layer
shadows and highlights onto a shape. It also has an alpha compositing ink
mode which allows you to blend colors together, and an alpha lock mode to
enable painting only inside existing shapes on a layer.
Aseprite does focus on animation, but it’s not without tile creation tools. It has an excellent tiled mode, whereby you can have an image repeat itself horizontally, vertically, or in both directions, allowing you to draw directly over seams to create seamless tiles.
Aseprite also has very useful color mode settings that allow you to choose between RGB mode for full color images, grayscale for black and white, or indexed mode where your colors will be confined to those in the currently active palette.
If
you draw in indexed mode any color you choose will automatically
replace itself with the closest color in your active palette. If you
draw in RGB mode then switch to indexed mode your image will be converted to
use colors from that palette only. This can be a great way to allow yourself to draw with a little more freedom, then refine your color palette down to be more crisp at the end of the process.
Aseprite has a color replacement tool, but in contrast with Pyxel Edit’s color replace tool where you draw over the pixels you want to update, in Aseprite you just provide two colors and it will auto replace all pixels of the specified color at once. I personally feel both methods have advantages.
Aseprite was originally GPL so you can still get that open source version free of charge from GitHub, but it won’t have the latest features. The most recent version is available for $14.99, and works across all three desktop operating systems.
Piskel is very straight
forward and to the point sprite and animation creation software. If you
want to get in and out fast this is a great solution. It’s so quick, in
fact, that it only takes about two and a half minutes for the essentials to be
explained in this video:
The focus of Piskel is definitely sprite creation and animation as opposed to tilesets or complete scene creation. It has a relatively limited set of tools, but it also has a direct approach that can be very efficient for sprite creation and animation. It’s made particularly convenient given it has a fully fledged online version, meaning you can access it anywhere.
After drawing your initial character sprite, animations
are created easily by just hitting the Add new frame button to the left of the canvas and
adjusting an FPS slider to control your speed. Onion skinning is
included, which is always a valuable feature in animation.
One thing to bear in mind is Piskel’s pen tool can only be set
to between 1px and 4px in size, meaning this software really is best for
focusing on sprites and not large pieces like landscapes where you
want to paint large areas such as skies quickly.
Of the common pixel art features you always like to have access to, Piskel includes a 100% preview in the right sidebar, it has rectangle, ellipse and stroke/lines tools, a vertical mirror pen for symmetrical drawing, and ellipse and shape selection tools. It also has a dithering tool, something very helpful yet surprisingly rare in pixel art applications.
Additionally it has a really great “shape selection” tool, which will identify any block of connected pixels of the
same color. It’s comparable to a magic wand tool, but is a little
simpler to use and also allows you to drag the selected shape around immediately after selection without needing to
switch to another tool.
As
well as a regular fill tool, Piskel also has a modified fill tool that will
alter the color of all pixels that share the same color, whether they’re
connected to one another or not. This is yet another approach to color replacement, the third we’ve seen in our article so far, and something incredibly helpful in
those moments when you’re fine tuning to get just the right color.
Another
fantastic tool is the lighten/darken tool which semi-automates the process of creating highlights and shadows. Draw directly over existing
pixels with this tool to create highlights, or hold CTRL to instead create shadows. You can
either progressively shift the color of a pixel as you draw back and
forwards over it, or you can hold down SHIFT to ensure the color is only
tinted or shaded one step at a time.
Piskel describes itself as an online app, which can be very convenient, but if you’d rather work offline you can also get desktop versions for Linux, Mac and Windows.
GIMP is known primarily as an image manipulation program, so it’s perhaps ironic that I didn’t recommend it in our series entry on photo editing software but I am recommending it here. I personally find it to be fantastic for pixel art, with the right setup. I based my own setup on the video you see above, which is quite old now, but still an excellent starting point for pixel art in GIMP.
GIMP doesn’t have some of the pixel art specific tools that dedicated software like Pyxel Edit or Aseprite have, but it does have some general tools that I find very helpful when doing pixel art. For example, GIMP’s selection tool. Once you draw out a selection the marquee has transformation handles that make it easy to select the exact pixels you need. Its floating window mode is something many people turn off right away, but with it active you can create a second view into your document at actual size and nestle it right into your layout.
GIMP’s color indexing mode can also be very useful for pixel art. If you’re under time pressure and need to produce fast, you can draw something with a more traditional painting technique in RGB mode then convert it to indexed mode to have it translated into a restricted color palette.
I scaled it down, with no interpolation, to get it to a size where the pixels will be nice and chunky. Then in the main application menu I went to Image > Mode > Interpolated and allowed a 256 color palette to be generated for me, with no dithering. Then I scaled the image back up, again with no interpolation, to the original size. Here’s the before and after:
This could still use some cleaning up to give it the crispness expected from pixel art, but it’s certainly off to a running start. Strictly speaking this is not pixel art, but for large projects like games you might need to create such a large number of assets that some shortcuts here and there are helpful.
You can also do things like switching in between RGB mode and indexed mode while working in order to speed certain things along. For example, you can draw out a gradient while in RGB mode, which will transition smoothly between the colors. Then you can switch to Indexed mode and enforce a color palette to get a pixel art friendly gradient effect instead.
Another helpful feature for game pixel artists is GIMP’s new tiling symmetry mode. You’ll need version 2.9.5 (compiled from source via Github) to access this. What it does is allow you to repeat whatever you are drawing every certain number of pixels. So if you are drawing a 16x16 tile, set the symmetry offset to 16px on both axis then you can ensure you have seamless tiling as you work:
GIMP isn’t a dedicated pixel art application, but it has a lot to offer and even if you don’t use it as your primary design software, you still might find some of these features slot right into your workflow.
Krita might not seem like the obvious choice when it comes to pixel art but in my experience it can be fantastic. Out of the box it includes a selection of dedicated pixel art brushes, including a square brush, a round brush and a dithering brush.
A big help is the wrap around feature, making seamless tile creation a smooth experience. Just hit W on the keyboard and your canvas tiles in both directions, allowing you to paint over any seams. Another thing I find helpful in Krita is having a fully fledged set of high level color selection tools for fine tuning of color palettes.
Krita also has an excellent alpha locking system, allowing you to do things like drawing out a shape on one layer, then nesting other layers so their content is only visible through the previous layer’s shape–a sort of masking, essentially. This makes adding shadows, highlights and details a much easier process, and allows for a workflow uncommon in pixel art apps.
Part of why I find Krita enjoyable for pixel art is its excellent support for a drawing tablet, while on the other hand I’ve found some pixel art applications have lag when using a tablet, or just overall feel more mouse and keyboard oriented.
Index painting is also available in Krita as it is in GIMP, with the advantage it can be used along with non destructive filter layers. However the setup for this technique is somewhat complex, as detailed in this tutorial.
And another feature Krita has is some basic vector tools you can use to draw out irregular shapes then have them automatically stroked with your selected pixel brush, a technique you can see used in the YouTube video at the start of this section. This is functionality you won’t typically find in pixel art applications.
Krita does have basic frame by frame animation tools but they’re still very new, and probably not the number one choice for pixel animation just yet. But Krita development is always moving fast, so keep an eye out!
For the creation of complete artworks however, Krita is a real prospect.
In my personal experience, the two strongest applications on this list are Pyxel Edit and Aseprite, and I don’t feel you can really separate which of the two is “best”. This is because if you are involved in game development and the creation of tilesets you’ll find Pyxel Edit’s specialization very helpful. But if you’re a character and animation artist, you’ll find Aseprite’s specialization very helpful.
Both applications can do some of what the other specializes in, though I’d say Pyxel Edit is probably stronger in animation than Aseprite is strong in tileset creation. However Aseprite is probably stronger for self contained artwork creation than Pyxel Edit. Overall I’d suggest if you’re involved in multiple types of pixel art, using the two together might serve you well.
I would class Piskel as a great application for when you want to get something done fast, or for when you’re on the move and need to access a quality pixel art editor online.
As for GIMP and Krita, in reality they can’t offer the same set of pixel art specific features that the aforementioned applications can. However they do offer a whole gamut of other features, the kind that come with software meant for fully fledged image editing or digital art. Whether you want to use one of these programs for pixel art will likely depend on how much you enjoy each one’s overall toolset and UI.
For me I particularly enjoy that in each application I can use a pencil brush and my drawing tablet to sketch out a rough draft of what I want to create as though I were working on paper, then I can draw my pixel art over the top. And GIMP’s ability to process images into indexed mode is particularly useful. Neither Krita nor GIMP are really top tier options for tile creation or sprite
animation, but they can be great for drawing self contained artworks.
That brings us to the end of our roundup of Adobe Photoshop alternatives for pixel art. With a bit of luck you’ve found an application in this list that you’re considering taking on as your new favorite pixel art editor!
Up Next: Animation
Adobe has two animation tools in its suite at the moment: Animate (formerly Flash) which allows for skeletal and keyframe based animation, and Photoshop which also allows for keyframe animation. However, just as with all the other software specialty fields we’ve looked at so far, there are several options for you to choose from when it comes to animation.
In the next article we’ll be checking out some of the best software available for skeletal, keyframe, and frame by frame animation. Some of these programs have fantastic specialist tools that will make your life a whole lot easier while creating animations.
The demo shows the end result of the slope implementation. Use WASD to move the character. Right mouse button creates a tile. You can use the scroll wheel or the arrow keys to select a tile you want to place. The sliders change the size of the player's character.
The demo has been published under Unity 5.5.2f1, and the source code is also compatible with this version of Unity.
Slopes
Slopes add a lot of versatility to a game, both in terms of possible interactions with the game's terrain and in visual variance, but their implementation can be very complex, especially if we'd like to support a vast number of slope types.
As was true for the previous parts in the series, we'll be continuing our work from the moment we left off the last part, even though we'll be reworking a big chunk of the code we've already written. What we'll need from the start is a working moving character and a tilemap.
You can download the project files from the previous part and write the code along with this tutorial.
Changes in Movement Integration
Since making the slopes work is pretty difficult, it'd be nice if we could actually make things easier in some aspects. Some time ago I stumbled upon a blog post on how Matt Thorson handles the physics in his games. Basically, in this method the movement is always made in 1px intervals. If a movement for a particular frame is larger than one pixel, then the movement vector is split into many 1px movements, and after each one the conditions for collision with the terrain are checked.
This saves us the headache of trying to find obstacles along the line of movement at once, and instead, we can do it iteratively. This makes the implementation simpler, but unfortunately it also increases the number of collision checks performed, so it might be inappropriate for games where there are many moving objects, especially high-resolution games where naturally the speed at which the objects are moving is higher. The plus side is that even though there will be more collision checks, each check will be much simpler since it knows that the character moves by a single pixel each time.
Slopes Data
Let's start defining the data that we'll need to represent the slopes. First of all, we'll need a height map of a slope, which will define its shape. Let's start with a classic 45-degree slope.
Let's also define another slope shape; this one will serve as more of a bump on the ground than anything else.
Of course we will want to use variants of these slopes, depending where we'd like to place them. For example, in the case of our defined 45-degree slope, it will fit nicely if there's a solid block to its right, but if the solid block is on its left then we'd like to use a flipped version of the tile we defined. We'll need to be able to flip the slopes on the X axis and Y axis as well as rotate them by 90 degrees to be able to access all the variants of a predefined slope.
Let's look at what the transformations of the 45-degree slope look like.
As you can see, in this case we can get all the variants using flips. We don't really need to rotate the slope by 90 degrees, but let's see how things look for the second slope we defined earlier.
In this case, the 90-degree rotation transform makes it possible to place the slope on the wall.
Calculate Offsets
Let's use our defined data to calculate the offsets which will need to be applied to the object that is overlapping with a tile. The offset will carry the information about:
how much an object needs to move up/down/left/right in order not to collide with a tile
how much an object needs to move to be right next to the top/bottom/left/right surface of a slope
The green parts of the above image are the parts where the object overlaps with the empty parts of the tile, and the yellow squares indicate the area in which the object overlaps with the slope.
Now, let's start to see how we'd calculate the offset for case number 1.
The object does not collide with any part of the slope. That means we don't really need to move it out of collision, so the first part of our offset will be set to 0.
For the second part of the offset, if we want the bottom of the object to be touching the slope, we would need to move it 3 pixels down. If we wanted the object's right side to touch the slope, we would need to move it 3 pixels to the right. For the object's left side to touch the right edge of the slope, we'd need to move it 16 pixels to the right. Similarly, if we wanted the top edge of the object to touch the slope, we'd need to move the object 16 pixels down.
Now, why would we need the information of how much distance there is between the object's edge and the slope? This data will be very useful to us when we want an object to stick to the slope.
So, for example, let's say an object moves left on our 45-degree slope. If it moves fast enough it will end up in the air, and then eventually it will fall on the slope again, and so on. If we want it to remain on the slope, each time it moves left, we'll want to push it down so it remains in touch with the slope. The below animation shows the difference between having slope sticking enabled or disabled for a character.
We'll be caching a lot of data here—basically, we want to calculate an offset for every possible overlap with a tile. This means that for every position and for every overlap size, we'll have a quick reference of how much to move an object. Note that we cannot cache the final offsets because we can't cache an offset for every possible AABB, but it's easy to adjust the offset knowing the AABB's overlap with the slope's tile.
Defining Tiles
We'll be defining all the slope data in a static Slopes class.
public static class Slopes
{
}
First of all, let's handle the heightmaps. Let's define a few of them to process later on.
Let's also create another enumeration for tile collision type. This will be useful for assigning the same collision type to different tiles, for example a grassy 45-degree slope or stone 45-degree slope.
Before we start calculating the offsets, we'll want to unfold our heightmaps into full collision bitmaps. This will make it easy to determine whether an AABB is colliding with a tile and also will enable more complex tile shapes if that's what we need. Let's create an array for those bitmaps.
Now let's create a function which will extend the heightmap into the bitmap.
public static sbyte[][] Extend(sbyte[] slope)
{
sbyte[][] extended = new sbyte[Map.cTileSize][];
for (int x = 0; x < Map.cTileSize; ++x)
{
extended[x] = new sbyte[Map.cTileSize];
for (int y = 0; y < Map.cTileSize; ++y)
extended[x][y] = System.Convert.ToSByte(y < slope[x]);
}
return extended;
}
Nothing complicated here—if a particular position on the tile is solid, we set it to 1; if it's not, it's set to 0.
Now let's create our Init function, which will eventually do all the caching work we need to have done on the slopes.
public static void Init()
{
}
Let's create the container arrays here.
public static void Init()
{
slopesHeights = new sbyte[(int)TileCollisionType.Count][];
slopesExtended = new sbyte[(int)TileCollisionType.Count][][];
}
Now let's make every tile collision type point to the corresponding cached data.
for (int i = 0; i < (int)TileCollisionType.Count; ++i)
{
switch ((TileCollisionType)i)
{
case TileCollisionType.Empty:
slopesHeights[i] = empty;
slopesExtended[i] = Extend(slopesHeights[i]);
break;
case TileCollisionType.Full:
slopesHeights[i] = full;
slopesExtended[i] = Extend(slopesHeights[i]);
break;
case TileCollisionType.Slope45:
slopesHeights[i] = slope45;
slopesExtended[i] = Extend(slopesHeights[i]);
break;
case TileCollisionType.Slope45FX:
case TileCollisionType.Slope45FY:
case TileCollisionType.Slope45FXY:
case TileCollisionType.Slope45F90:
case TileCollisionType.Slope45F90X:
case TileCollisionType.Slope45F90XY:
case TileCollisionType.Slope45F90Y:
slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45];
slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45];
break;
case TileCollisionType.SlopeMid1:
slopesHeights[i] = slopeMid1;
slopesExtended[i] = Extend(slopesHeights[i]);
break;
case TileCollisionType.SlopeMid1FX:
case TileCollisionType.SlopeMid1FY:
case TileCollisionType.SlopeMid1FXY:
case TileCollisionType.SlopeMid1F90:
case TileCollisionType.SlopeMid1F90X:
case TileCollisionType.SlopeMid1F90XY:
case TileCollisionType.SlopeMid1F90Y:
slopesHeights[i] = slopesHeights[(int)TileCollisionType.SlopeMid1];
slopesExtended[i] = slopesExtended[(int)TileCollisionType.SlopeMid1];
break;
}
}
As explained before, the freeLeft, freeRight, freeDown, and freeUp variables correspond to the offset that needs to be applied so the object is no longer colliding with the slope, while the collidingLeft, collidingRight, collidingTop, and collidingBottom are the distance that the object needs to be shifted to touch the slope while not overlapping it.
It's time to create our heavy-duty caching function, but just before we do it, let's create a container which will hold all that data.
public static sbyte[][] slopesHeights;
public static sbyte[][][] slopesExtended;
public static SlopeOffsetSB[][][][][] slopeOffsets;
And create the array in the Init function.
slopesHeights = new sbyte[(int)TileCollisionType.Count][];
slopesExtended = new sbyte[(int)TileCollisionType.Count][][];
slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][];
Memory Issues
As you can see, this array has plenty of dimensions, and each new tile type will actually require quite a lot of memory. For every X position in the tile, for every Y position in the tile, for every possible Width in the tile and for every possible Height, there will be a separate offset value calculation.
Since the tiles we are using are 16x16, this means that the amount of data needed for each tile type will be 16*16*16*16*8 bytes, which equals 512 kB. This is a lot of data, but still manageable, and of course if caching this amount of information is unfeasible, we'll need to either switch to calculating the offsets in real time, probably using a more efficient method than the one we're using for caching, or optimize our data.
Right now, if the tile size in our game was bigger, say 32x32, each tile type would occupy 8 MB, and if we used 64x64, then it would be 128MB. These amounts seem way too big to be useful, especially if we want to have quite a few slope types in the game. A sensible solution to this seems to be splitting the big collision tiles into smaller ones. Note that it is just each newly defined slope that requires more space—the transformations use the same data.
Checking Collisions Within a Tile
Before we start calculating the offsets, we need to know if an object at a particular position will collide with the solid parts of the tile. Let's create this function first.
public static bool Collides(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h)
{
for (int x = posX; x <= posX + w && x < Map.cTileSize; ++x)
{
for (int y = posY; y <= posY + h && y < Map.cTileSize; ++y)
{
if (slopeExtended[x][y] == 1)
return true;
}
}
return false;
}
The function takes the collision bitmap, the position of the overlap, and the overlap size. The position is the bottom left pixel of the object, and the size is the 0-based width and height. By 0-based, I mean that width of 0 means that the object is actually 1 pixel wide, and width equal to 15 means that the object is 16 pixels wide. The function is very simple—if any of the object's pixels overlap with a slope, then we return true, otherwise we return false.
Now let's calculate how much we need to move the object to make it not collide with the slope. To do that, while the object is colliding with the slope we need to keep moving it up and checking for collision until there's no overlap with the solid parts of the tile.
Above is the illustration of how we calculate the offset. In the first case, since the object is touching the top bound of the tile, instead of just moving it up we also need to decrease its height. That's because if any part of the AABB moves outside the tile bounds, we are no longer interested in it. Similarly, offsets are calculated for all other directions, so for the above example the offsets would be:
4 for the up offset
-4 for the left offset
-16 for the down offset—that's the maximum distance because basically if we move the object down, we need to move it all the way out of the bounds of the tile to stop colliding with the slope
16 for the right offset
Let's start by declaring the temporary variable for the height of the object. As mentioned above, this will change depending on how high we'll be moving the object.
sbyte movH = h;
Now it's time for the main condition. As long as the object hasn't moved out of the tile bounds and it collides with the solid parts of the tile, we need to increase the offsetUp.
Now let's do the same thing for the left offset. Note that when we're moving the object left and the object is being moved out of the tile bounds, we don't really need to alter the position; instead, we just change the width of the overlap. This is illustrated on the right side of the animation illustrating the offset calculation.
But here, since we weren't moving the freeLeft offset along the way as we were decreasing the width, we need to convert the reduced size into the offset.
Alright, we've calculated the first part of the offset—that is how much we should move the object for it to stop colliding with the slope. Now it's time to figure out the offsets which are supposed to move the object right next to the solid parts of the tile.
Notice that if we need to move the object out of collision, we're already doing that, because we stop right after the collision is no more.
In the case on the right, the up offset is 4, but it is also the offset that we need to move the object for its bottom edge to sit on a solid pixel. The same goes for the other sides.
if (freeUp == 0)
{
}
else
collidingBottom = freeUp;
Now the case on the left is where we need to find the offsets ourselves. If we want to find the collidingBottom offset there, we need to move the object 3 pixels down. The calculations needed here are similar to previous ones, but instead this time we'll be looking for when the object will collide with the slope, and then moving while reducing the offset by one, so it barely touches the solid pixels instead of overlapping them.
If freeUp is equal the 0, free down must be equal to 0 as well, so we can throw in the calculations for collidingTop under the same brackets. Again, these calculations are analogous to what we've been doing so far.
Now that all the offsets are calculated, we can return the offset for this particular data set.
return new SlopeOffsetSB(freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop);
Let's create a container for all our cached data.
public static sbyte[][] slopesHeights;
public static sbyte[][][] slopesExtended;
public static SlopeOffsetSB[][][][][] slopeOffsets;
Initialize the array.
slopesHeights = new sbyte[(int)TileCollisionType.Count][];
slopesExtended = new sbyte[(int)TileCollisionType.Count][][];
slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][];
And finally, create the caching function.
public static SlopeOffsetSB[][][][] CacheSlopeOffsets(sbyte[][] slopeExtended)
{
var offsetCache = new SlopeOffsetSB[Map.cTileSize][][][];
for (int x = 0; x < Map.cTileSize; ++x)
{
offsetCache[x] = new SlopeOffsetSB[Map.cTileSize][][];
for (int y = 0; y < Map.cTileSize; ++y)
{
offsetCache[x][y] = new SlopeOffsetSB[Map.cTileSize][];
for (int w = 0; w < Map.cTileSize; ++w)
{
offsetCache[x][y][w] = new SlopeOffsetSB[Map.cTileSize];
for (int h = 0; h < Map.cTileSize; ++h)
{
offsetCache[x][y][w][h] = GetOffset(slopeExtended, (sbyte)x, (sbyte)y, (sbyte)w, (sbyte)h);
}
}
}
}
return offsetCache;
}
The function itself is very simple, so it's very easy to see how much data it caches to satisfy our requirements!
Now make sure to cache the offsets for each tile collision type.
case TileCollisionType.Slope45:
slopesHeights[i] = slope45;
slopesExtended[i] = Extend(slopesHeights[i]);
slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]);
break;
case TileCollisionType.Slope45FX:
case TileCollisionType.Slope45FY:
case TileCollisionType.Slope45FXY:
case TileCollisionType.Slope45F90:
case TileCollisionType.Slope45F90X:
case TileCollisionType.Slope45F90XY:
case TileCollisionType.Slope45F90Y:
slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45];
slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45];
slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45];
break;
And that's it, our main caching function is finished!
Calculating the World Space Offset
Now let's use the cached data to make a function which will return an offset for a character which exists in a world space.
The offset that we'll be returning is not the same struct we used for the cached data, since the world space offsets can end up being bigger than the limits of the single byte. The structure is basically the same thing, but using integers.
The parameters are as follows:
the world space center of the tile
the left, right, bottom and top edges of the AABB we want to receive the offset for
the type of tile we want to receive the offset for
First, we need to figure out how the AABB overlaps with a slope tile. We need to know where the overlap starts (the bottom left corner), and also how much the overlap extends over the tile.
To calculate this, let's first declare the variables.
Now this should be quite easy. There are two main categories of cases we can find here. First is that the overlap is within the tile bounds.
The dark blue pixel is the position of the overlap, and the height and width are marked with the blue tiles. Here things are pretty straightforward, so calculating the position and the size of the overlap doesn't require any additional actions.
The second category of cases looks as follows, and in the game we'll mostly be dealing with those cases:
Let's look at an example situation pictured above. As you can see, the AABB extends well beyond the tile, but what we need to figure out is the position and size of the overlap within the tile itself, so we can retrieve our cached offset value. Right now we don't really care about anything that lies beyond the tile bounds. This will require us to clamp the overlap position and size to the tile's bounds.
Position x is equal to the offset between the left edge of the AABB and the left edge of the tile. If AABB is to the left of the tile's left edge, the position needs to be clamped to 0. To get the overlap width, we need to subtract the AABB's right edge from the overlap's x position, which we already calculated.
The values for the Y axis are calculated in the same way.
Now we can retrieve the cached offsets for the overlap.
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]);
Adjust the Offset
Before we return the offset, we might need to adjust it. Consider the following situation.
Let's see how our cached offset for such an overlap would look. When caching, we were only concerned about the overlap within the tile bounds, so in this case, the up offset would be equal to 9. You can see that if we moved the overlap area within the tile bounds 9 pixels up, it would cease to collide with the slope, but if we move the whole AABB, then the area which is below the tile bounds will move into the collision.
Basically, what we need to do here is adjust the up offset by the number of pixels the AABB extends below the tile bounds.
if (bottomTileEdge > bottomY)
{
if (offset.freeUp > 0)
offset.freeUp += (int)bottomTileEdge - (int)bottomY;
offset.collidingBottom = offset.freeUp;
}
The same thing needs to be done for all of the other offsets—left, right, and down—except that for now we'll skip handling the left and right offsets in this manner since it is not necessary to do so.
if (topTileEdge < topY)
{
if (offset.freeDown < 0)
offset.freeDown -= (int)(topY - topTileEdge);
offset.collidingTop = offset.freeDown;
}
if (bottomTileEdge > bottomY)
{
if (offset.freeUp > 0)
offset.freeUp += (int)bottomTileEdge - (int)bottomY;
offset.collidingBottom = offset.freeUp;
}
Once we're done, we can return the adjusted offset. The finished function should look like this.
Of course, it's not completely done yet. Later on we'll also be handling the tile transformations here, so the offset is returned appropriately depending on whether the tile has been flipped on the XY axes or rotated 90 degrees. For now, though, we'll be playing only with the non-transformed tiles.
Implementing One-Pixel Step Physics
Overview
Moving the objects by one pixel will make it quite easy to handle a lot of things, especially collisions against slopes for fast objects, but even though we're going to check for collision each pixel we move, we should move in a specific pattern to ensure accuracy. This pattern will be dependent on the object's speed.
On the picture above, you can see that if we blindly move the object first all the pixels it needs to move horizontally, and after that vertically, the arrow would end up colliding with a solid block that's not really on its course. The order of movement should be based on the ratio of the vertical to horizontal speed; this way we'll know how many pixels we need to move vertically for each pixel moved horizontally.
Define the Data
Let's move to our moving object class and define a few new variables.
First of all, our main mPosition variable will be holding only the integer numbers, and we'll be keeping another variable called mRemainder to keep the value after the floating point.
public Vector2 mPosition;
public Vector2 mRemainder;
Next, we'll add a few new position status variables to indicate whether the character is currently on the slope. At this point, it will be good if we pack all the position status into a single structure.
[Serializable]
public struct PositionState
{
public bool pushesRight;
public bool pushesLeft;
public bool pushesBottom;
public bool pushesTop;
public bool pushedTop;
public bool pushedBottom;
public bool pushedRight;
public bool pushedLeft;
public bool pushedLeftObject;
public bool pushedRightObject;
public bool pushedBottomObject;
public bool pushedTopObject;
public bool pushesLeftObject;
public bool pushesRightObject;
public bool pushesBottomObject;
public bool pushesTopObject;
public bool pushedLeftTile;
public bool pushedRightTile;
public bool pushedBottomTile;
public bool pushedTopTile;
public bool pushesLeftTile;
public bool pushesRightTile;
public bool pushesBottomTile;
public bool pushesTopTile;
public bool onOneWayPlatform;
public Vector2i leftTile;
public Vector2i rightTile;
public Vector2i topTile;
public Vector2i bottomTile;
public void Reset()
{
leftTile = rightTile = topTile = bottomTile = new Vector2i(-1, -1);
pushesRight = false;
pushesLeft = false;
pushesBottom = false;
pushesTop = false;
pushedTop = false;
pushedBottom = false;
pushedRight = false;
pushedLeft = false;
pushedLeftObject = false;
pushedRightObject = false;
pushedBottomObject = false;
pushedTopObject = false;
pushesLeftObject = false;
pushesRightObject = false;
pushesBottomObject = false;
pushesTopObject = false;
pushedLeftTile = false;
pushedRightTile = false;
pushedBottomTile = false;
pushedTopTile = false;
pushesLeftTile = false;
pushesRightTile = false;
pushesBottomTile = false;
pushesTopTile = false;
onOneWayPlatform = false;
}
}
Now let's declare an instance of the struct for the object.
public PositionState mPS;
Another variable that we'll need is slope sticking.
public bool mSticksToSlope;
Basic Implementation
Let's start by creating the basic collision checking functions; these will not handle the slopes yet.
The parameters used here are the current position of the object, its top right and bottom left corners, and the position state. First of all, let's calculate the top right and top left tile for our object.
Now let's iterate through all the tiles along the object's right edge.
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
{
var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y);
}
Now, depending on the collision tile, we react appropriately.
switch (tileCollisionType)
{
default://slope
break;
case TileCollisionType.Empty:
break;
case TileCollisionType.Full:
state.pushesRightTile = true;
state.rightTile = new Vector2i(topRightTile.x, y);
return true;
}
As you can see, for now we'll skip handling the slopes; we just want to get the basic setup done before we delve into that.
Overall, the function for now should look like this:
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat)
{
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f));
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
{
var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y);
switch (tileCollisionType)
{
default://slope
break;
case TileCollisionType.Empty:
break;
case TileCollisionType.Full:
state.pushesRightTile = true;
state.rightTile = new Vector2i(topRightTile.x, y);
return true;
}
}
return false;
}
We'll do the same for all the other three directions: left, up, and down.
public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat)
{
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f));
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y)
{
var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y);
switch (tileCollisionType)
{
default://slope
break;
case TileCollisionType.Empty:
break;
case TileCollisionType.Full:
state.pushesLeftTile = true;
state.leftTile = new Vector2i(bottomLeftTile.x, y);
return true;
}
}
return false;
}
public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat)
{
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f));
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f));
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
{
var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y);
switch (tileCollisionType)
{
default://slope
break;
case TileCollisionType.Empty:
break;
case TileCollisionType.Full:
state.pushesTopTile = true;
state.topTile = new Vector2i(x, topRightTile.y);
return true;
}
}
return false;
}
public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat)
{
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f));
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f));
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x)
{
var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y);
switch (tileCollisionType)
{
default://slope
break;
case TileCollisionType.Empty:
break;
case TileCollisionType.Full:
state.onOneWayPlatform = false;
state.pushesBottomTile = true;
state.bottomTile = new Vector2i(x, bottomleftTile.y);
return true;
}
}
return false;
}
Moving Functions
Now that we have this covered, we can start creating two functions responsible for movement. One will handle the movement horizontally, and another will handle the vertical movement.
The arguments we'll use in this function are the current position, a boolean which indicates whether we find an obstacle along the way or not, an offset which defines how much we need to move, a step which is a value we move the object with each iteration, the AABB's lower left and upper right vertices, and finally the position state.
Basically, what we want to do here is move the object by a step so many times, so that the steps sum up to the offset. Of course, if we meet an obstacle, we need to stop moving as well.
while (!foundObstacleX && offset != 0.0f)
{
}
With each iteration, we subtract the step from the offset, so the offset eventually becomes zero, and we know we moved as many pixels as we needed to.
With each step, we want to check whether we collide with a tile. If we're moving right, we want to check if we collide with a wall on the right; if we're moving left, we want to check for obstacles on the left.
Finally, after we move, we check for collisions up and down, because we could slide right under or above a block. This is just to update the position state to be accurate.
The function takes the value of how much to move the object, the object's current speed, its current position together with the floating point remainder, the object's AABB, and the position state.
The first thing we'll do here is add the offset to the remainder, so that in the remainder we have the full value of how much our character should move.
remainder += offset;
Since we'll be calling the MoveX and MoveY functions from this one, we'll need to pass the top right and bottom left corners of the AABB, so let's calculate them now.
We also need to get the step vector. It will be used as a direction in which we'll be moving our object.
var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y));
Now let's see how many pixels we actually need to move. We need to round the remainder, because we are always going to move by an integer number, and then we need to subtract that value from the remainder.
var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y));
remainder -= move;
Now let's split the movement into four cases, depending on our move vector values. If the move vector's x and y values are equal to 0, there is no movement to be made, so we can just return.
if (move.x == 0.0f && move.y == 0.0f)
return;
If only the y value is 0, we're going to move only horizontally.
And after this we can move vertically. Here we know that we need to move the object by the value contained in the vertAccum, but in case of any inaccuracies, if we moved all the way on the X axis, we also need to move all the way on the Y axis.
Now we can use the functions we've built to compose our main UpdatePhysics function.
Build the Physics Update Function
First of all, we want to update the position state, so all of the previous frame's data goes to the adequate variables, and the current frame's data is reset.
Now let's update the collision state of our object. We do this so that before we move our object, we have updated data on whether it's on the ground or is pushing any other tiles. Normally the previous frame's data would still be up to date if the terrain was unmodifiable and other objects wouldn't be able to move this one, but here we assume that any of this could happen.
That's it! This system now replaces the older one, the results should be the same, although the way we do it is quite a bit different.
Summary
That's it for laying down the groundwork for the slopes, so what's left is to fill up those gaps in our collision checks! We've done most of our caching work here and eliminated a lot of geometrical complexities by implementing the one-pixel movement integration.
This will make the slope implementation a breeze, compared to what we'd need to do otherwise. We'll be finishing the job in the next part of the series.