We've been building our library of courses for three years, and now have over 400 professional video courses. With a mix of screencast lessons and live video, it's a fantastic way to learn a new skill. Our courses cover:
Whether you're interested in learning to draw, becoming a Photoshop expert, improving your web design skills, learning a new programming language, or starting out as a photographer, you'll find a course to help further your passion.
In-Depth Video Training
If you regularly read and enjoy our tutorial content, we think you'll love the extra depth offered in video courses. You'll be taken through important concepts and techniques in detail, accompanied with practical examples and advice.
Here's an example from one of our recent photography lessons on flash photography:
Start Your Free 14 Day Trial
Our subscription plans start from just $15 per month, but we're now offering a completely free, 14 day trial! Head over to our pricing page to sign up, access all our 400+ courses, and begin learning straight away.
At any time, you can choose to upgrade to a Yearly or Yearly Pro subscription, giving you additional access to eBooks, downloadable videos, subscriber benefits, and much more.
We're confident that a Tuts+ subscription will help you further your education and career, so don't miss the opportunity to give it a try for free!
There are plenty of uses for lightning effects in games, from background ambiance during a storm to the devastating lightning attacks of a sorcerer. In this tutorial, I'll explain how to programmatically generate awesome 2D lightning effects: bolts, branches, and even text.
This tutorial is written specifically for Unity, with all code snippets in JavaScript. The same tutorial is also available with C# code. If you don't use Unity, take a look at this platform-agnostic version of the same tutorial; it's written for XNA, but you should be able to use the same techniques and concepts in any gamedev engine and platform.
Basic Setup
To get started, you'll need to create a new 2D project in Unity. Name it whatever you'd like. In Unity, create four folders: Materials,Prefabs,Scripts,and Sprites.
Next, click the Main Camera and make sure that its Projection is set to Orthographic. Set the camera's Size to 10.
Right-click on the Materials folder and select Create > Material. Rename it to Additive. Select this material and change its Shader to Particles > Additive. This will help your lightning "pop" later on.
Step 1: Draw a Glowing Line
The basic building block we need to make lightning from is a line segment. Start by opening your favourite image editing software and drawing a straight line of lightning with a glow effect. Here's what mine looks like:
We want to draw lines of different lengths, so we will cut the line segment into three pieces as shown below (crop your image as necessary). This will allow us to stretch the middle segment to any length we like. Since we are going to be stretching the middle segment, we can save it as only a single pixel thick. Also, as the left and right pieces are mirror images of each other, we only need to save one of them; we can flip it in the code.
Drag your image files onto the Sprites folder in the Project panel. This will import the image files into the Unity project. Click on the sprites to view them in the Inspector panel. Make sure the Texture Type is set to Sprite(2D \ uGUI), and set the Packing Tag to Line.
The Packing Tag will help Unity save on draw calls when drawing our lightning, so make sure you give both sprites the same Packing Tag or else it won't improve performance.
Now, let's declare a new class to handle drawing line segments:
#pragma strict
class LineJS extends MonoBehaviour
{
//Start
public var A: Vector2;
//End
public var B: Vector2;
//Thickness of line
public var Thickness: float;
//Children that contain the pieces that make up the line
public var StartCapChild : GameObject;
public var LineChild : GameObject;
public var EndCapChild : GameObject;
//Create a new line
public function Line(a : Vector2, b : Vector2, thickness : float)
{
A = a;
B = b;
Thickness = thickness;
}
//Used to set the color of the line
public function SetColor(color : Color)
{
StartCapChild.GetComponent(SpriteRenderer).color = color;
LineChild.GetComponent(SpriteRenderer).color = color;
EndCapChild.GetComponent(SpriteRenderer).color = color;
}
//...
}
A and B are the line's endpoints. By scaling and rotating the pieces of the line, we can draw a line of any thickness, length, and orientation.
Add the following Draw() method to the bottom of the LineJS class:
//Will actually draw the line
public function Draw()
{
var difference : Vector2 = B - A;
var rotation : float = Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg;
//Set the scale of the line to reflect length and thickness
LineChild.transform.localScale = new Vector3(100 * (difference.magnitude / LineChild.GetComponent(SpriteRenderer).sprite.rect.width),
Thickness,
LineChild.transform.localScale.z);
StartCapChild.transform.localScale = new Vector3(StartCapChild.transform.localScale.x,
Thickness,
StartCapChild.transform.localScale.z);
EndCapChild.transform.localScale = new Vector3(EndCapChild.transform.localScale.x,
Thickness,
EndCapChild.transform.localScale.z);
//Rotate the line so that it is facing the right direction
LineChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation));
StartCapChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation));
EndCapChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation + 180));
//Move the line to be centered on the starting point
LineChild.transform.position = new Vector3 (A.x, A.y, LineChild.transform.position.z);
StartCapChild.transform.position = new Vector3 (A.x, A.y, StartCapChild.transform.position.z);
EndCapChild.transform.position = new Vector3 (A.x, A.y, EndCapChild.transform.position.z);
//Need to convert rotation to radians at this point for Cos/Sin
rotation *= Mathf.Deg2Rad;
//Store these so we only have to access once
var lineChildWorldAdjust : float = LineChild.transform.localScale.x * LineChild.GetComponent(SpriteRenderer).sprite.rect.width / 2f;
var startCapChildWorldAdjust : float = StartCapChild.transform.localScale.x * StartCapChild.GetComponent(SpriteRenderer).sprite.rect.width / 2f;
var endCapChildWorldAdjust : float = EndCapChild.transform.localScale.x * EndCapChild.GetComponent(SpriteRenderer).sprite.rect.width / 2f;
//Adjust the middle segment to the appropriate position
LineChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * lineChildWorldAdjust,
.01f * Mathf.Sin(rotation) * lineChildWorldAdjust,
0);
//Adjust the start cap to the appropriate position
StartCapChild.transform.position -= new Vector3 (.01f * Mathf.Cos(rotation) * startCapChildWorldAdjust,
.01f * Mathf.Sin(rotation) * startCapChildWorldAdjust,
0);
//Adjust the end cap to the appropriate position
EndCapChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * lineChildWorldAdjust * 2,
.01f * Mathf.Sin(rotation) * lineChildWorldAdjust * 2,
0);
EndCapChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * endCapChildWorldAdjust,
.01f * Mathf.Sin(rotation) * endCapChildWorldAdjust,
0);
}
The way we position the middle segment and the caps will make them join seamlessly when we draw them. The start cap is positioned at point A, the middle segment is stretched to the desired width, and the end cap is rotated 180° and drawn at point B.
Now we need to create a prefab for our LineJS class to work with. In Unity, from the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to LineJS and drag your LineJS script onto it. It should look something like the image below.
We'll use this object as a container for the pieces of our line segment.
Now we need to create objects for the pieces of our line segment. Create three Sprites by selecting GameObject > Create Other > Sprite from the menu. Rename them toStartCap, MiddleSegment, and EndCap. Drag them onto our LineJS object so that they become its children—this should look something like the image below.
Go through each child and set its Material in the Sprite Renderer to the Additive material we created earlier. Assign each child the appropriate sprite. (The two caps should get the cap sprite and the middle segment should get the line sprite.)
Click on the LineJS object so that you can see the script in the Inspector panel. Assign the children to their appropriate slots and then drag the LineJS object into the Prefabs folder to create a prefab for it. You can now delete the LineJS object from the Hierarchy panel.
Step 2: Create Jagged Lines
Lightning tends to form jagged lines, so we'll need an algorithm to generate these. We'll do this by picking points at random along a line, and displacing them a random distance from the line.
Using a completely random displacement tends to make the line too jagged, so we'll smooth the results by limiting how far from each other neighbouring points can be displaced—see the difference between the second and third lines in the figure below.
We smooth the line by placing points at a similar offset to the previous point; this allows the line as a whole to wander up and down, while preventing any part of it from being too jagged.
Let's create a LightningBoltJS class to handle creating our jagged lines.
#pragma strict
import System.Collections.Generic;
class LightningBoltJS extends MonoBehaviour
{
//List of all of our active/inactive lines
public var ActiveLineObj : List.<GameObject>;
public var InactiveLineObj : List.<GameObject>;
//Prefab for a line
public var LinePrefab : GameObject;
//Transparency
public var Alpha : float;
//The speed at which our bolts will fade out
public var FadeOutRate : float;
//The color of our bolts
public var Tint : Color;
//The position where our bolt started
public function Start()
{
var first : GameObject = ActiveLineObj[0];
return first.GetComponent(LineJS).A;
}
//The position where our bolt ended
public function End()
{
var last : GameObject = ActiveLineObj[ActiveLineObj.Count-1];
return last.GetComponent(LineJS).B;
}
//True if the bolt has completely faded out
public function IsComplete()
{
return Alpha <= 0;
}
public function Initialize(maxSegments : int)
{
//Initialize lists for pooling
ActiveLineObj = new List.<GameObject>();
InactiveLineObj = new List.<GameObject>();
for(var i : int = 0; i < maxSegments; i++)
{
//instantiate from our Line Prefab
var line : GameObject = GameObject.Instantiate(LinePrefab);
//parent it to our bolt object
line.transform.parent = transform;
//set it inactive
line.SetActive(false);
//add it to our list
InactiveLineObj.Add(line);
}
}
public function ActivateBolt(source : Vector2, dest : Vector2, color : Color, thickness : float)
{
//for use in loops later
var i : int;
//Store tint
Tint = color;
//Store alpha
Alpha = 1.5f;
//Store fade out rate
FadeOutRate = 0.03f;
//actually create the bolt
//Prevent from getting a 0 magnitude
if(Vector2.Distance(dest, source) <= 0)
{
var adjust : Vector2 = Random.insideUnitCircle;
if(adjust.magnitude <= 0) adjust.x += .1f;
dest += adjust;
}
//difference from source to destination
var slope : Vector2 = dest - source;
var normal : Vector2 = (new Vector2(slope.y, -slope.x)).normalized;
//distance between source and destination
var distance : float = slope.magnitude;
var positions : List.<float> = new List.<float>();
positions.Add(0);
for (i = 0; i < distance / 4; i++)
{
//Generate random positions between 0 and 1 to break up the bolt
//positions.Add (Random.Range(0f, 1f));
positions.Add(Random.Range(.25f, .75f));
}
positions.Sort();
var Sway : float = 80;
var Jaggedness : float = 1 / Sway;
//Affects how wide the bolt is allowed to spread
var spread : float = 1f;
//Start at the source
var prevPoint : Vector2 = source;
//No previous displacement, so just 0
var prevDisplacement : float = 0;
for (i = 1; i < positions.Count; i++)
{
//don't allow more than we have in the pool
var inactiveCount : int = InactiveLineObj.Count;
if(inactiveCount <= 0) break;
var pos : float = positions[i];
var prevPos : float = positions[i - 1];
//used to prevent sharp angles by ensuring very close positions also have small perpendicular variation.
var scale : float = (distance * Jaggedness) * (pos - prevPos);
//defines an envelope. Points near the middle of the bolt can be further from the central line.
var envelope : float = pos > 0.95f ? 20 * (1 - pos) : spread;
//calculate the displacement
var displacement : float = Random.Range(-Sway, Sway);
displacement -= (displacement - prevDisplacement) * (1 - scale);
displacement *= envelope;
//Calculate the end point
var point : Vector2 = source + (pos * slope) + (displacement * normal);
activateLine(prevPoint, point, thickness);
prevPoint = point;
prevDisplacement = displacement;
}
activateLine(prevPoint, dest, thickness);
}
public function DeactivateSegments()
{
for(var i : int = ActiveLineObj.Count - 1; i >= 0; i--)
{
var line : GameObject = ActiveLineObj[i];
line.SetActive(false);
ActiveLineObj.RemoveAt(i);
InactiveLineObj.Add(line);
}
}
function activateLine(A : Vector2, B : Vector2, thickness : float)
{
//get the inactive count
var inactiveCount : int = InactiveLineObj.Count;
//only activate if we can pull from inactive
if(inactiveCount <= 0) return;
//pull the GameObject
var lineObj : GameObject = InactiveLineObj[InactiveLineObj.Count - 1];
//set it active
lineObj.SetActive(true);
//get the Line component
var lineComponent : LineJS = lineObj.GetComponent(LineJS);
lineComponent.SetColor(Color.white);
lineComponent.A = A;
lineComponent.B = B;
lineComponent.Thickness = thickness;
ActiveLineObj.Add(lineObj);
InactiveLineObj.Remove(lineObj);
}
public function Draw()
{
//if the bolt has faded out, no need to draw
if (Alpha <= 0) return;
for(var i : int = 0; i < ActiveLineObj.Count; i++)
{
var obj : GameObject = ActiveLineObj[i];
var lineComponent : LineJS = obj.GetComponent(LineJS);
lineComponent.SetColor(Tint * (Alpha * 0.6f));
lineComponent.Draw();
}
}
public function Update()
{
Alpha -= FadeOutRate;
}
//...
}
The code may look a bit intimidating, but it's not so bad once you understand the logic. Before we continue on, understand that we've chosen to pool our line segments in the bolts (since constantly instantiating and destroying objects can be costly in Unity).
The Initialize() function will be called once on each lightning bolt and will determine how many line segments each bolt is allowed to use.
The activateLine() function will activate a line segment using the given position data.
The DeactivateSegments() function will deactivate any active line segments in our bolt.
The ActivateBolt() function will handle creating our jagged lines and will call the activateLine() function to activate our line segments at the appropriate positions.
To create our jagged lines, we start by computing the slope between our two points, as well as the normal vector to that slope. We then choose a number of random positions along the line and store them in our positions list. We scale these positions between 0 and 1, such that 0 represents the start of the line and 1 represents the end point., and then sort these positions to allow us to easily add line segments between them.
The loop goes through the randomly chosen points and displaces them along the normal by a random amount. The scale factor is there to avoid overly sharp angles, and theenvelope ensures the lightning actually goes to the destination point by limiting displacement when we're close to the end. The spread is to assist in controlling how far the segments deviate from the slope of our line; a spread of 0 will essentially give you a straight line.
So, like we did with our LineJS class, let's make this into a prefab. From the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to BoltJS, and drag a copy of the LightningBoltJS script onto it. Finally, click on the BoltJS object and assign the LineJS prefab, from the Prefabs folder, to the appropriate slot in the LightningBoltJS script. Once you're done with that, simply drag the BoltJS object into the Prefabs folder to create a prefab.
Step 3: Add Animation
Lightning should flash brightly and then fade out. This is what our Update() and Draw() functions in LightningBoltJS are for. Calling Update() will make the bolt fade out. Calling Draw() will update the bolt's color on the screen. IsComplete() will tell you when the bolt has fully faded out.
Step 4: Create a Bolt
Now that we have our LightningBoltJS class, let's actually put it to good use and set up a quick demo scene.
We're going to use an object pool for this demo, so we'll want to create an empty object to hold our active and inactive bolts (simply for organizational purposes). In Unity, from the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to LightningPoolHolder.
Right click on the Scripts folder and select Create > Javascript. Name your script DemoScriptJS and open it. Here's some quick code to get you started:
#pragma strict
import System.Collections.Generic;
class DemoScriptJS extends MonoBehaviour
{
//Prefabs to be assigned in Editor
public var BoltPrefab : GameObject;
//For pooling
var activeBoltsObj : List.<GameObject>;
var inactiveBoltsObj : List.<GameObject>;
var maxBolts : int = 1000;
//For handling mouse clicks
var clicks : int = 0;
var pos1 : Vector2;
var pos2 : Vector2;
function Start()
{
//Initialize lists
activeBoltsObj = new List.<GameObject>();
inactiveBoltsObj = new List.<GameObject>();
//for use later
var tempV3 : Vector3;
//Grab the parent we'll be assigning to our bolt pool
var p : GameObject = GameObject.Find("LightningPoolHolder");
//For however many bolts we've specified
for(var i : int = 0; i < maxBolts; i++)
{
//create from our prefab
var bolt : GameObject = Instantiate(BoltPrefab);
//Assign parent
bolt.transform.parent = p.transform;
//Initialize our lightning with a preset number of max sexments
bolt.GetComponent(LightningBoltJS).Initialize(25);
//Set inactive to start
bolt.SetActive(false);
//Store in our inactive list
inactiveBoltsObj.Add(bolt);
}
}
function Update()
{
//Declare variables for use later
var boltObj : GameObject;
var boltComponent : LightningBoltJS;
var i : int;
var tempV3 : Vector3;
var adjust : Vector2;
//store off the count for effeciency
var activeLineCount : int = activeBoltsObj.Count;
//loop through active lines (backwards because we'll be removing from the list)
for (i = activeLineCount - 1; i >= 0; i--)
{
//pull GameObject
boltObj = activeBoltsObj[i];
//get the LightningBolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//if the bolt has faded out
if(boltComponent.IsComplete())
{
//deactive the segments it contains
boltComponent.DeactivateSegments();
//set it inactive
boltObj.SetActive(false);
//move it to the inactive list
activeBoltsObj.RemoveAt(i);
inactiveBoltsObj.Add(boltObj);
}
}
//If left mouse button pressed
if(Input.GetMouseButtonDown(0))
{
//if first click
if(clicks == 0)
{
//store starting position
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos1 = new Vector2(tempV3.x, tempV3.y);
}
else if(clicks == 1) //second click
{
//store end position
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos2 = new Vector2(tempV3.x, tempV3.y);
CreatePooledBolt(pos1,pos2, Color.white, 1f);
}
//increment our tick count
clicks++;
//restart the count after 2 clicks
if(clicks > 1) clicks = 0;
}
//update and draw active bolts
for(i = 0; i < activeBoltsObj.Count; i++)
{
boltObj = activeBoltsObj[i];
boltObj.GetComponent(LightningBoltJS).Update();
boltObj.GetComponent(LightningBoltJS).Draw();
}
}
function CreatePooledBolt(source : Vector2, dest : Vector2, color : Color, thickness : float)
{
//if there is an inactive bolt to pull from the pool
if(inactiveBoltsObj.Count > 0)
{
//pull the GameObject
var boltObj : GameObject = inactiveBoltsObj[inactiveBoltsObj.Count - 1];
//set it active
boltObj.SetActive(true);
//move it to the active list
activeBoltsObj.Add(boltObj);
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1);
//get the bolt component
var boltComponent : LightningBoltJS = boltObj.GetComponent(LightningBoltJS);
//activate the bolt using the given position data
boltComponent.ActivateBolt(source, dest, color, thickness);
}
}
}
All this code does is give us a way to create bolts using object pooling. There are other ways you can set this up, but this is what we're going with! Once we've set it up, all you'll have to do is click twice to create a bolt on the screen: once for the start position and once for the end position.
We'll need an object to put our DemoScriptJS on. From the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to DemoScript and drag your DemoScriptJS script onto it. Click on the DemoScript object so we can view it in the Inspector panel. Assign the BoltJS prefab, from the Prefabs folder, to the matching slot in the DemoScriptJS.
That should be enough to get you going! Run the scene in Unity and try it out!
Step 5: Create Branch Lightning
You can use the LightningBoltJS class as a building block to create more interesting lightning effects. For example, you can make the bolts branch out as shown below:
To make the lightning branch, we pick random points along the lightning bolt and add new bolts that branch out from these points. In the code below, we create between three and six branches which separate from the main bolt at 30° angles.
#pragma strict
import System.Collections.Generic;
class BranchLightningJS extends MonoBehaviour
{
//For holding all of our bolts in our branch
public var boltsObj : List.<GameObject>;
//If there are no bolts, then the branch is complete (we're not pooling here, but you could if you wanted)
public function IsComplete()
{
return boltsObj.Count <= 0;
}
//Start position of branch
public var Start : Vector2;
//End position of branch
public var End : Vector2;
static var rand : Random = new Random();
public function Initialize(start : Vector2, end : Vector2, boltPrefab : GameObject)
{
//for use lateer
var i : int;
//store start and end positions
Start = start;
End = end;
//create the main bolt from our bolt prefab
var mainBoltObj : GameObject = GameObject.Instantiate(boltPrefab);
//get the LightningBolt component
var mainBoltComponent : LightningBoltJS = mainBoltObj.GetComponent(LightningBoltJS);
//initialize our bolt with a max of 5 segments
mainBoltComponent.Initialize(5);
//activate the bolt with our position data
mainBoltComponent.ActivateBolt(start, end, Color.white, 1f);
//add it to our list
boltsObj.Add(mainBoltObj);
//randomly determine how many sub branches there will be (3-6)
var numBranches : int = Random.Range(3,6);
//calculate the difference between our start and end points
var diff : Vector2 = end - start;
// pick a bunch of random points between 0 and 1 and sort them
var branchPoints : List.<float> = new List.<float>();
for(i = 0; i < numBranches; i++) branchPoints.Add(Random.value);
branchPoints.Sort();
//go through those points
for (i = 0; i < branchPoints.Count; i++)
{
// Bolt.GetPoint() gets the position of the lightning bolt based on the percentage passed in (0 = start of bolt, 1 = end)
var boltStart : Vector2 = mainBoltComponent.GetPoint(branchPoints[i]);
//get rotation of 30 degrees. Alternate between rotating left and right. (i & 1 will be true for all odd numbers...yay bitwise operators!)
var rot : Quaternion = Quaternion.AngleAxis(30 * ((i & 1) == 0 ? 1 : -1), new Vector3(0,0,1));
var point : float = branchPoints[i];
//calculate how much to adjust for our end position
var adjust : Vector2 = rot * (Random.Range(.5f, .75f) * diff * (1 - point));
//get the end position
var boltEnd : Vector2 = adjust + boltStart;
//instantiate from our bolt prefab
var boltObj : GameObject = GameObject.Instantiate(boltPrefab);
//get the LightningBolt component
var boltComponent : LightningBoltJS = boltObj.GetComponent(LightningBoltJS);
//initialize our bolt with a max of 5 segments
boltComponent.Initialize(5);
//activate the bolt with our position data
boltComponent.ActivateBolt(boltStart, boltEnd, Color.white, 1f);
//add it to the list
boltsObj.Add(boltObj);
}
}
public function Update()
{
//go through our active bolts
for (var i : int = boltsObj.Count - 1; i >= 0; i--)
{
//get the GameObject
var boltObj : GameObject = boltsObj[i];
//get the LightningBolt component
var boltComp : LightningBoltJS = boltObj.GetComponent(LightningBoltJS);
//update/fade out the bolt
boltComp.Update();
//if the bolt has faded
if(boltComp.IsComplete())
{
//remove it from our list
boltsObj.RemoveAt(i);
//destroy it (would be better to pool but I'll let you figure out how to do that =P)
Destroy(boltObj);
}
}
}
//Draw our active bolts on screen
public function Draw()
{
var boltObj : GameObject;
for(var i : int; i < boltsObj.Count; i++)
{
boltObj = boltsObj[i];
boltObj.GetComponent(LightningBoltJS).Draw();
}
}
}
This code works very similarly to our LightningBoltJS class with the exception that it does not use object pooling. Calling Initialize() is all you will need to do to create a branching bolt; after that, you will just need to call Update() and Draw(). I'll show you exactly how to do this in our DemoScriptJS later on in the tutorial.
You may have noticed the reference to a GetPoint() function in the LightningBoltJS class. We haven't actually implemented that function yet, so let's take care of that now.
Add the following function in the bottom of the LightningBoltJS class:
// Returns the point where the bolt is at a given fraction of the way through the bolt. Passing
// zero will return the start of the bolt, and passing 1 will return the end.
public function GetPoint(position : float)
{
var start : Vector2 = Start();
var length : float = Vector2.Distance(start, End());
var dir : Vector2 = (End() - start) / length;
position *= length;
var line : LineJS;
//find the appropriate line
for(var i : int = 0; i < ActiveLineObj.Count; i++)
{
var x : GameObject = ActiveLineObj[i];
if(Vector2.Dot(x.GetComponent(LineJS).B - start, dir) >= position)
{
line = x.GetComponent(LineJS);
break;
}
}
var lineStartPos : float = Vector2.Dot(line.A - start, dir);
var lineEndPos : float = Vector2.Dot(line.B - start, dir);
var linePos : float = (position - lineStartPos) / (lineEndPos - lineStartPos);
return Vector2.Lerp(line.A, line.B, linePos);
}
Step 6: Create Lightning Text
Below is a video of another effect you can make out of the lightning bolts:
We'll need to do a little more setup for this one. First, from the Project panel, select Create > RenderTexture. Rename it to RenderText and set its Size to 256x256px. (It doesn't necessarily have to be that exact size, but the smaller it is, the faster the program will run.)
From the menu, select Edit > Project Settings > Tags and Layers. Then, in the Inspector panel, expand the Layers drop down and add Text into User Layer 8.
We'll then need to create a second camera. From the menu, select GameObject > Create Other > Camera. Rename it to TextCamera, and set its Projection to Orthographic and its Clear Flags to Solid Color. Set its Background color to(R: 0, G: 0, B: 0, A: 0) and set its Culling Mask to only be Text (the layer we just created). Finally, set its Target Texture to RenderText (the RenderTexture we created earlier). You'll probably need to play around with the camera's Size later, in order to get everything to fit on the screen.
Now we'll need to create the actual text we'll be drawing with our lightning. From the menu select GameObject > Create Other > GUI Text. Select the GUI Text object from the Hierarchy panel and set its Text to LIGHTNING, its Anchor to middle center, and its Alignment to center. Then, set its Layer to the Text layer we created earlier. You'll probably have to play around with the Font Size in order to fit the text on the screen.
Now select the Main Camera and set its Culling Mask to be everything but our Text layer. This will cause our GUI Text to apparently disappear from the screen, but it should be drawn on the RenderTexture we created earlier: select RenderText from the Project panel and you should be able to see the word LIGHTNING on the preview on the bottom of the panel.
If you can't see the word LIGHTNING, you'll need to play around with your positioning, font size, and (text) camera size. To help you position your text, click on TextCamera in the Hierarchy panel, and set its Target Texture to None. You'll now be able to see your GUI Text if you center it on the TextCamera. Once you have everything positioned, set the TextCamera's Target Texture back to RenderText.
Now for the code! We'll need to get the pixels from the text that we're drawing. We can do this by drawing our text to a RenderTarget and reading back the pixel data into aTexture2D with Texture2D.ReadPixels(). Then, we can store the coordinates of the pixels from the text as a List.<Vector2>.
Here's the code to do that:
//Capture the important points of our text for later
function TextCapture()
{
//must wait until end of frame so something is actually drawn or else it will error
yield WaitForEndOfFrame();
//get the camera that draws our text
var cam : Camera = GameObject.Find("TextCamera").GetComponent(Camera);
//make sure it has an assigned RenderTexture
if(cam.targetTexture != null)
{
//pull the active RenderTexture
RenderTexture.active = cam.targetTexture;
//capture the image into a Texture2D
var image : Texture2D = new Texture2D(cam.targetTexture.width, cam.targetTexture.height);
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0);
image.Apply();
//calculate how the text will be scaled when it is displayed as lightning on the screen
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x);
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
positionText.x -= image.width * scaleText * .5f;
positionText.y -= image.height * scaleText * .5f;
//basically determines how many pixels we skip/check
var interval : int = 2;
//loop through pixels
for(var y : int = 0; y < image.height; y += interval)
{
for(var x : int = 0; x < image.width; x += interval)
{
//get the color of the pixel
var color : Color = image.GetPixel(x,y);
//if the color has an r (red) value
if(color.r > 0)
{
//add it to our points for drawing
textPoints.Add(new Vector2(x,y));
}
}
}
}
}
Note: We'll have to run this function as a Coroutine at the start of our program in order for it to run correctly.
After that, each frame, we can randomly pick pairs of these points and create a lightning bolt between them. We want to design it so that the closer two points are to one another, the greater the chance is that we create a bolt between them.
There's a simple technique we can use to accomplish this: we'll pick the first point at random, and then we'll pick a fixed number of other points at random and choose the nearest.
Here's the code for that (we'll add it to our DemoScriptJS later):
//go through the points we capture earlier
for (var i1 : int = 0; i1 < textPoints.Count; i1++)
{
var point : Vector2 = textPoints[i1];
//randomly ignore certain points
if(Random.Range(0,75) != 0) continue;
//placeholder values
var nearestParticle : Vector2 = Vector2.zero;
var nearestDistSquared : float = float.MaxValue;
for (i = 0; i < 50; i++)
{
//select a random point
var other : Vector2 = textPoints[Random.Range(0, textPoints.Count)];
//calculate the distance (squared for performance benefits) between the two points
var distSquared : float = DistanceSquared(point, other);
//If this point is the nearest point (but not too near!)
if (distSquared < nearestDistSquared && distSquared > 3 * 3)
{
//store off the data
nearestDistSquared = distSquared;
nearestParticle = other;
}
}
//if the point we found isn't too near/far
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3)
{
//create a (pooled) bolt at the corresponding screen position
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f);
}
}
/* The code above uses the following function
* It'll need to be placed appropriately
---------------------------------------------
//calculate distance squared (no square root = performance boost)
public function DistanceSquared(a : Vector2, b : Vector2)
{
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
---------------------------------------------*/
The number of candidate points we test will affect the look of the lightning text; checking a larger number of points will allow us to find very close points to draw bolts between, which will make the text very neat and legible, but with fewer long lightning bolts between letters. Smaller numbers will make the lightning text look wilder but less legible.
Step 7: Try Other Variations
We've discussed making branch lightning and lightning text, but those certainly aren't the only effects you can make. Let's look at a couple other variations on lightning you may want to use.
Moving Lightning
You may often want to make a moving bolt of lightning. You can do this by adding a new short bolt each frame at the end point of the previous frame's bolt.
//Will contain all of the pieces for the moving bolt
var movingBolt : List.<GameObject>;
//used for actually moving the moving bolt
var lightningEnd : Vector2 = new Vector2(100, 100);
var lightningVelocity : Vector2 = new Vector2(1, 0);
function Update()
{
//loop through all of our bolts that make up the moving bolt
for(i = movingBolt.Count - 1; i >= 0; i--)
{
boltObj = movingBolt[i];
//get the bolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//if the bolt has faded out
if(boltComponent.IsComplete())
{
//destroy it
Destroy(movingBolt[i]);
//remove it from our list
movingBolt.RemoveAt(i);
//on to the next one, on on to the next one
continue;
}
//update and draw bolt
boltComponent.Update();
boltComponent.Draw();
}
//if our moving bolt is active
if(movingBolt.Count > 0)
{
boltObj = movingBolt[movingBolt.Count-1];
//calculate where it currently ends
lightningEnd = boltObj.GetComponent(LightningBoltJS).End();
//if the end of the bolt is within 25 units of the camera
if(Vector2.Distance(lightningEnd,Camera.main.transform.position) < 25)
{
//instantiate from our bolt prefab
boltObj = GameObject.Instantiate(BoltPrefab);
//get the bolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//initialize it with a maximum of 5 segments
boltComponent.Initialize(5);
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f);
//add it to our list
movingBolt.Add(boltObj);
//update and draw our new bolt
boltComponent.Update();
boltComponent.Draw();
}
}
}
Burst Lightning
This variation offers a dramatic effect that shoots lightning out in a circle from the centre point:
//get the difference between our two positions (destination - source = vector from source to destination)
var diff : Vector2 = pos2 - pos1;
function Update()
{
//define how many bolts we want in our circle
var boltsInBurst : int = 10;
for(i = 0; i < boltsInBurst; i++)
{
//rotate around the z axis to the appropriate angle
var rot : Quaternion = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1));
adjust = rot * diff;
//Calculate the end position for the bolt
var boltEnd : Vector2 = adjust + pos1;
//create a (pooled) bolt from pos1 to boltEnd
CreatePooledBolt(pos1, boltEnd, Color.white, 1f);
}
}
Step 8: Put It All Together in DemoScriptJS
You're going to want to be able to try out all of these fancy effects we've created so far, so let's put all of them into the DemoScriptJS we made earlier. You'll be able to toggle between effects by hitting the number keys on your keyboard to select the effect, and then just clicking twice like we did with our bolts before.
Here's the full code:
#pragma strict
#pragma strict
import System.Collections.Generic;
class DemoScriptJS extends MonoBehaviour
{
//Prefabs to be assigned in Editor
public var BoltPrefab : GameObject;
public var BranchPrefab : GameObject;
//For pooling
var activeBoltsObj : List.<GameObject>;
var inactiveBoltsObj : List.<GameObject>;
var maxBolts : int = 1000;
var scaleText : float;
var positionText : Vector2;
//Different modes for the demo
class Mode
{
public static final var bolt : byte = 0;
public static final var branch : byte = 1;
public static final var moving : byte = 2;
public static final var text : byte = 3;
public static final var nodes : byte = 4;
public static final var burst : byte = 5;
}
//The current mode the demo is in
var currentMode : byte = Mode.bolt;
//Will contain all of the pieces for the moving bolt
var movingBolt : List.<GameObject>;
//used for actually moving the moving bolt
var lightningEnd : Vector2 = new Vector2(100, 100);
var lightningVelocity : Vector2 = new Vector2(1, 0);
//Will contain all of the pieces for the branches
var branchesObj : List.<GameObject>;
//For handling mouse clicks
var clicks : int = 0;
var pos1 : Vector2;
var pos2 : Vector2;
//For storing all of the pixels that need to be drawn by the bolts
var textPoints : List.<Vector2>;
//true in text mode
var shouldText : boolean = false;
function Start()
{
//Initialize lists
activeBoltsObj = new List.<GameObject>();
inactiveBoltsObj = new List.<GameObject>();
branchesObj = new List.<GameObject>();
//for use later
var tempV3 : Vector3;
//Grab the parent we'll be assigning to our bolt pool
var p : GameObject = GameObject.Find("LightningPoolHolder");
//For however many bolts we've specified
for(var i : int = 0; i < maxBolts; i++)
{
//create from our prefab
var bolt : GameObject = Instantiate(BoltPrefab);
//Assign parent
bolt.transform.parent = p.transform;
//Initialize our lightning with a preset number of max sexments
bolt.GetComponent(LightningBoltJS).Initialize(25);
//Set inactive to start
bolt.SetActive(false);
//Store in our inactive list
inactiveBoltsObj.Add(bolt);
}
//Start up a coroutine to capture the pixels we'll be drawing from our text (need the coroutine or error)
StartCoroutine(TextCapture());
}
function Update()
{
//Declare variables for use later
var boltObj : GameObject;
var boltComponent : LightningBoltJS;
var i : int;
var tempV3 : Vector3;
var adjust : Vector2;
var branchObj : GameObject;
var branchComponent : BranchLightningJS;
//store off the count for effeciency
var activeLineCount : int = activeBoltsObj.Count;
//loop through active lines (backwards because we'll be removing from the list)
for (i = activeLineCount - 1; i >= 0; i--)
{
//pull GameObject
boltObj = activeBoltsObj[i];
//get the LightningBolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//if the bolt has faded out
if(boltComponent.IsComplete())
{
//deactive the segments it contains
boltComponent.DeactivateSegments();
//set it inactive
boltObj.SetActive(false);
//move it to the inactive list
activeBoltsObj.RemoveAt(i);
inactiveBoltsObj.Add(boltObj);
}
}
//check for key press and set mode accordingly
if(Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
shouldText = false;
currentMode = Mode.bolt;
}
else if(Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
shouldText = false;
currentMode = Mode.branch;
}
else if(Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
shouldText = false;
currentMode = Mode.moving;
}
else if(Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
{
shouldText = true;
currentMode = Mode.text;
}
else if(Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
{
shouldText = false;
currentMode = Mode.nodes;
}
else if(Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6))
{
shouldText = false;
currentMode = Mode.burst;
}
//If left mouse button pressed
if(Input.GetMouseButtonDown(0))
{
//if first click
if(clicks == 0)
{
//store starting position
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos1 = new Vector2(tempV3.x, tempV3.y);
}
else if(clicks == 1) //second click
{
//store end position
tempV3 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos2 = new Vector2(tempV3.x, tempV3.y);
//Handle the current mode appropriately
switch (currentMode)
{
case Mode.bolt:
//create a (pooled) bolt from pos1 to pos2
CreatePooledBolt(pos1,pos2, Color.white, 1f);
break;
case Mode.branch:
//instantiate from our branch prefab
branchObj = GameObject.Instantiate(BranchPrefab);
//get the branch component
branchComponent = branchObj.GetComponent(BranchLightningJS);
//initialize the branch component using our position data
branchComponent.Initialize(pos1, pos2, BoltPrefab);
//add it to the list of active branches
branchesObj.Add(branchObj);
break;
case Mode.moving:
//Prevent from getting a 0 magnitude (0 causes errors
if(Vector2.Distance(pos1, pos2) <= 0)
{
//Try a random position
adjust = Random.insideUnitCircle;
//failsafe
if(adjust.magnitude <= 0) adjust.x += .1f;
//Adjust the end position
pos2 += adjust;
}
//Clear out any old moving bolt (this is designed for one moving bolt at a time)
for(i = movingBolt.Count - 1; i >= 0; i--)
{
Destroy(movingBolt[i]);
movingBolt.RemoveAt(i);
}
//get the "velocity" so we know what direction to send the bolt in after initial creation
lightningVelocity = (pos2 - pos1).normalized;
//instantiate from our bolt prefab
boltObj = GameObject.Instantiate(BoltPrefab);
//get the bolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//initialize it with 5 max segments
boltComponent.Initialize(5);
//activate the bolt using our position data
boltComponent.ActivateBolt(pos1, pos2, Color.white, 1f);
//add it to our list
movingBolt.Add(boltObj);
break;
case Mode.burst:
//get the difference between our two positions (destination - source = vector from source to destination)
var diff : Vector2 = pos2 - pos1;
//define how many bolts we want in our circle
var boltsInBurst : int = 10;
for(i = 0; i < boltsInBurst; i++)
{
//rotate around the z axis to the appropriate angle
var rot : Quaternion = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1));
adjust = rot * diff;
//Calculate the end position for the bolt
var boltEnd : Vector2 = adjust + pos1;
//create a (pooled) bolt from pos1 to boltEnd
CreatePooledBolt(pos1, boltEnd, Color.white, 1f);
}
break;
}
}
//increment our tick count
clicks++;
//restart the count after 2 clicks
if(clicks > 1) clicks = 0;
}
//if in node mode
if(currentMode == Mode.nodes)
{
//constantly create a (pooled) bolt between the two assigned positions
CreatePooledBolt(pos1, pos2, Color.white, 1f);
}
//loop through any active branches
for(i = branchesObj.Count - 1; i >= 0; i--)
{
branchObj = branchesObj[i];
//pull the branch lightning component
branchComponent = branchObj.GetComponent(BranchLightningJS);
//If it's faded out already
if(branchComponent.IsComplete())
{
//destroy it
Destroy(branchesObj[i]);
//take it out of our list
branchesObj.RemoveAt(i);
//move on to the next branch
continue;
}
//draw and update the branch
branchComponent.Update();
branchComponent.Draw();
}
//loop through all of our bolts that make up the moving bolt
for(i = movingBolt.Count - 1; i >= 0; i--)
{
boltObj = movingBolt[i];
//get the bolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//if the bolt has faded out
if(boltComponent.IsComplete())
{
//destroy it
Destroy(movingBolt[i]);
//remove it from our list
movingBolt.RemoveAt(i);
//on to the next one, on on to the next one
continue;
}
//update and draw bolt
boltComponent.Update();
boltComponent.Draw();
}
//if our moving bolt is active
if(movingBolt.Count > 0)
{
boltObj = movingBolt[movingBolt.Count-1];
//calculate where it currently ends
lightningEnd = boltObj.GetComponent(LightningBoltJS).End();
//if the end of the bolt is within 25 units of the camera
if(Vector2.Distance(lightningEnd,Camera.main.transform.position) < 25)
{
//instantiate from our bolt prefab
boltObj = GameObject.Instantiate(BoltPrefab);
//get the bolt component
boltComponent = boltObj.GetComponent(LightningBoltJS);
//initialize it with a maximum of 5 segments
boltComponent.Initialize(5);
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f);
//add it to our list
movingBolt.Add(boltObj);
//update and draw our new bolt
boltComponent.Update();
boltComponent.Draw();
}
}
//if in text mode
if(shouldText)
{
//go through the points we capture earlier
for (var i1 : int = 0; i1 < textPoints.Count; i1++)
{
var point : Vector2 = textPoints[i1];
//randomly ignore certain points
if(Random.Range(0,75) != 0) continue;
//placeholder values
var nearestParticle : Vector2 = Vector2.zero;
var nearestDistSquared : float = float.MaxValue;
for (i = 0; i < 50; i++)
{
//select a random point
var other : Vector2 = textPoints[Random.Range(0, textPoints.Count)];
//calculate the distance (squared for performance benefits) between the two points
var distSquared : float = DistanceSquared(point, other);
//If this point is the nearest point (but not too near!)
if (distSquared < nearestDistSquared && distSquared > 3 * 3)
{
//store off the data
nearestDistSquared = distSquared;
nearestParticle = other;
}
}
//if the point we found isn't too near/far
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3)
{
//create a (pooled) bolt at the corresponding screen position
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f);
}
}
}
//update and draw active bolts
for(i = 0; i < activeBoltsObj.Count; i++)
{
boltObj = activeBoltsObj[i];
boltObj.GetComponent(LightningBoltJS).Update();
boltObj.GetComponent(LightningBoltJS).Draw();
}
}
//calculate distance squared (no square root = performance boost)
public function DistanceSquared(a : Vector2, b : Vector2)
{
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
function CreatePooledBolt(source : Vector2, dest : Vector2, color : Color, thickness : float)
{
//if there is an inactive bolt to pull from the pool
if(inactiveBoltsObj.Count > 0)
{
//pull the GameObject
var boltObj : GameObject = inactiveBoltsObj[inactiveBoltsObj.Count - 1];
//set it active
boltObj.SetActive(true);
//move it to the active list
activeBoltsObj.Add(boltObj);
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1);
//get the bolt component
var boltComponent : LightningBoltJS = boltObj.GetComponent(LightningBoltJS);
//activate the bolt using the given position data
boltComponent.ActivateBolt(source, dest, color, thickness);
}
}
//Capture the important points of our text for later
function TextCapture()
{
//must wait until end of frame so something is actually drawn or else it will error
yield WaitForEndOfFrame();
//get the camera that draws our text
var cam : Camera = GameObject.Find("TextCamera").GetComponent(Camera);
//make sure it has an assigned RenderTexture
if(cam.targetTexture != null)
{
//pull the active RenderTexture
RenderTexture.active = cam.targetTexture;
//capture the image into a Texture2D
var image : Texture2D = new Texture2D(cam.targetTexture.width, cam.targetTexture.height);
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0);
image.Apply();
//calculate how the text will be scaled when it is displayed as lightning on the screen
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x);
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
positionText.x -= image.width * scaleText * .5f;
positionText.y -= image.height * scaleText * .5f;
//basically determines how many pixels we skip/check
var interval : int = 2;
//loop through pixels
for(var y : int = 0; y < image.height; y += interval)
{
for(var x : int = 0; x < image.width; x += interval)
{
//get the color of the pixel
var color : Color = image.GetPixel(x,y);
//if the color has an r (red) value
if(color.r > 0)
{
//add it to our points for drawing
textPoints.Add(new Vector2(x,y));
}
}
}
}
}
}
Conclusion
Lightning is a great special effect for sprucing up your games. The effects described in this tutorial are a nice starting point, but it's certainly not all you can do with lightning. With a bit of imagination you can make all kinds of awe-inspiring lightning effects! Download the source code and experiment on your own.
There are plenty of uses for lightning effects in games, from background ambiance during a storm to the devastating lightning attacks of a sorcerer. In this tutorial, I'll explain how to programmatically generate awesome 2D lightning effects: bolts, branches, and even text.
This tutorial is written specifically for Unity, with all code snippets in C#. The same tutorial is also available with JavaScript code. If you don't use Unity, take a look at this platform-agnostic version of the same tutorial; it's written for XNA, but you should be able to use the same techniques and concepts in any gamedev engine and platform.
Basic Setup
To get started, you'll need to create a new 2D project in Unity. Name it whatever you'd like. In Unity, create four folders: Materials,Prefabs,Scripts,and Sprites.
Next, click the Main Camera and make sure that its Projection is set to Orthographic. Set the camera's Size to 10.
Right-click on the Materials folder and select Create > Material. Rename it to Additive. Select this material and change its Shader to Particles > Additive. This will help your lightning "pop" later on.
Step 1: Draw a Glowing Line
The basic building block we need to make lightning from is a line segment. Start by opening your favourite image editing software and drawing a straight line of lightning with a glow effect. Here's what mine looks like:
We want to draw lines of different lengths, so we will cut the line segment into three pieces as shown below (crop your image as necessary). This will allow us to stretch the middle segment to any length we like. Since we are going to be stretching the middle segment, we can save it as only a single pixel thick. Also, as the left and right pieces are mirror images of each other, we only need to save one of them; we can flip it in the code.
Drag your image files onto the Sprites folder in the Project panel. This will import the image files into the Unity project. Click on the sprites to view them in the Inspector panel. Make sure the Texture Type is set to Sprite(2D \ uGUI), and set the Packing Tag to Line.
The Packing Tag will help Unity save on draw calls when drawing our lightning, so make sure you give both sprites the same Packing Tag or else it won't improve performance.
Now, let's declare a new class to handle drawing line segments:
using UnityEngine;
using System.Collections;
public class Line : MonoBehaviour
{
//Start
public Vector2 A;
//End
public Vector2 B;
//Thickness of line
public float Thickness;
//Children that contain the pieces that make up the line
public GameObject StartCapChild, LineChild, EndCapChild;
//Create a new line
public Line(Vector2 a, Vector2 b, float thickness)
{
A = a;
B = b;
Thickness = thickness;
}
//Used to set the color of the line
public void SetColor(Color color)
{
StartCapChild.GetComponent<SpriteRenderer>().color = color;
LineChild.GetComponent<SpriteRenderer>().color = color;
EndCapChild.GetComponent<SpriteRenderer>().color = color;
}
//...
}
A and B are the line's endpoints. By scaling and rotating the pieces of the line, we can draw a line of any thickness, length, and orientation.
Add the following Draw() method to the bottom of the Line class:
//Will actually draw the line
public void Draw()
{
Vector2 difference = B - A;
float rotation = Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg;
//Set the scale of the line to reflect length and thickness
LineChild.transform.localScale = new Vector3(100 * (difference.magnitude / LineChild.GetComponent<SpriteRenderer>().sprite.rect.width),
Thickness,
LineChild.transform.localScale.z);
StartCapChild.transform.localScale = new Vector3(StartCapChild.transform.localScale.x,
Thickness,
StartCapChild.transform.localScale.z);
EndCapChild.transform.localScale = new Vector3(EndCapChild.transform.localScale.x,
Thickness,
EndCapChild.transform.localScale.z);
//Rotate the line so that it is facing the right direction
LineChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation));
StartCapChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation));
EndCapChild.transform.rotation = Quaternion.Euler(new Vector3(0,0, rotation + 180));
//Move the line to be centered on the starting point
LineChild.transform.position = new Vector3 (A.x, A.y, LineChild.transform.position.z);
StartCapChild.transform.position = new Vector3 (A.x, A.y, StartCapChild.transform.position.z);
EndCapChild.transform.position = new Vector3 (A.x, A.y, EndCapChild.transform.position.z);
//Need to convert rotation to radians at this point for Cos/Sin
rotation *= Mathf.Deg2Rad;
//Store these so we only have to access once
float lineChildWorldAdjust = LineChild.transform.localScale.x * LineChild.GetComponent<SpriteRenderer>().sprite.rect.width / 2f;
float startCapChildWorldAdjust = StartCapChild.transform.localScale.x * StartCapChild.GetComponent<SpriteRenderer>().sprite.rect.width / 2f;
float endCapChildWorldAdjust = EndCapChild.transform.localScale.x * EndCapChild.GetComponent<SpriteRenderer>().sprite.rect.width / 2f;
//Adjust the middle segment to the appropriate position
LineChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * lineChildWorldAdjust,
.01f * Mathf.Sin(rotation) * lineChildWorldAdjust,
0);
//Adjust the start cap to the appropriate position
StartCapChild.transform.position -= new Vector3 (.01f * Mathf.Cos(rotation) * startCapChildWorldAdjust,
.01f * Mathf.Sin(rotation) * startCapChildWorldAdjust,
0);
//Adjust the end cap to the appropriate position
EndCapChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * lineChildWorldAdjust * 2,
.01f * Mathf.Sin(rotation) * lineChildWorldAdjust * 2,
0);
EndCapChild.transform.position += new Vector3 (.01f * Mathf.Cos(rotation) * endCapChildWorldAdjust,
.01f * Mathf.Sin(rotation) * endCapChildWorldAdjust,
0);
}
The way we position the middle segment and the caps will make them join seamlessly when we draw them. The start cap is positioned at point A, the middle segment is stretched to the desired width, and the end cap is rotated 180° and drawn at point B.
Now we need to create a prefab for our Line class to work with. In Unity, from the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to Line and drag your Line script onto it. It should look something like the image below.
We'll use this object as a container for the pieces of our line segment.
Now we need to create objects for the pieces of our line segment. Create three Sprites by selecting GameObject > Create Other > Sprite from the menu. Rename them to StartCap, MiddleSegment, and EndCap. Drag them onto our Line object so that they become its children—this should look something like the image below.
Go through each child and set its Material in the Sprite Renderer to the Additive material we created earlier. Assign each child the appropriate sprite. (The two caps should get the cap sprite and the middle segment should get the line sprite.)
Click on the Line object so that you can see the script in the Inspector panel. Assign the children to their appropriate slots and then drag the Line object into the Prefabs folder to create a prefab for it. You can now delete the Line object from the Hierarchy panel.
Step 2: Create Jagged Lines
Lightning tends to form jagged lines, so we'll need an algorithm to generate these. We'll do this by picking points at random along a line, and displacing them a random distance from the line.
Using a completely random displacement tends to make the line too jagged, so we'll smooth the results by limiting how far from each other neighbouring points can be displaced—see the difference between the second and third lines in the figure below.
We smooth the line by placing points at a similar offset to the previous point; this allows the line as a whole to wander up and down, while preventing any part of it from being too jagged.
Let's create a LightningBolt class to handle creating our jagged lines.
using UnityEngine;
using System.Collections.Generic;
class LightningBolt : MonoBehaviour
{
//List of all of our active/inactive lines
public List<GameObject> ActiveLineObj;
public List<GameObject> InactiveLineObj;
//Prefab for a line
public GameObject LinePrefab;
//Transparency
public float Alpha { get; set; }
//The speed at which our bolts will fade out
public float FadeOutRate { get; set; }
//The color of our bolts
public Color Tint { get; set; }
//The position where our bolt started
public Vector2 Start { get { return ActiveLineObj[0].GetComponent<Line>().A; } }
//The position where our bolt ended
public Vector2 End { get { return ActiveLineObj[ActiveLineObj.Count-1].GetComponent<Line>().B; } }
//True if the bolt has completely faded out
public bool IsComplete { get { return Alpha <= 0; } }
public void Initialize(int maxSegments)
{
//Initialize lists for pooling
ActiveLineObj = new List<GameObject>();
InactiveLineObj = new List<GameObject>();
for(int i = 0; i < maxSegments; i++)
{
//instantiate from our Line Prefab
GameObject line = (GameObject)GameObject.Instantiate(LinePrefab);
//parent it to our bolt object
line.transform.parent = transform;
//set it inactive
line.SetActive(false);
//add it to our list
InactiveLineObj.Add(line);
}
}
public void ActivateBolt(Vector2 source, Vector2 dest, Color color, float thickness)
{
//Store tint
Tint = color;
//Store alpha
Alpha = 1.5f;
//Store fade out rate
FadeOutRate = 0.03f;
//actually create the bolt
//Prevent from getting a 0 magnitude
if(Vector2.Distance(dest, source) <= 0)
{
Vector2 adjust = Random.insideUnitCircle;
if(adjust.magnitude <= 0) adjust.x += .1f;
dest += adjust;
}
//difference from source to destination
Vector2 slope = dest - source;
Vector2 normal = (new Vector2(slope.y, -slope.x)).normalized;
//distance between source and destination
float distance = slope.magnitude;
List<float> positions = new List<float>();
positions.Add(0);
for (int i = 0; i < distance / 4; i++)
{
//Generate random positions between 0 and 1 to break up the bolt
//positions.Add (Random.Range(0f, 1f));
positions.Add (Random.Range(.25f, .75f));
}
positions.Sort();
const float Sway = 80;
const float Jaggedness = 1 / Sway;
//Affects how wide the bolt is allowed to spread
float spread = 1f;
//Start at the source
Vector2 prevPoint = source;
//No previous displacement, so just 0
float prevDisplacement = 0;
for (int i = 1; i < positions.Count; i++)
{
//don't allow more than we have in the pool
int inactiveCount = InactiveLineObj.Count;
if(inactiveCount <= 0) break;
float pos = positions[i];
//used to prevent sharp angles by ensuring very close positions also have small perpendicular variation.
float scale = (distance * Jaggedness) * (pos - positions[i - 1]);
//defines an envelope. Points near the middle of the bolt can be further from the central line.
float envelope = pos > 0.95f ? 20 * (1 - pos) : spread;
float displacement = Random.Range(-Sway, Sway);
displacement -= (displacement - prevDisplacement) * (1 - scale);
displacement *= envelope;
//Calculate the end point
Vector2 point = source + (pos * slope) + (displacement * normal);
activateLine(prevPoint, point, thickness);
prevPoint = point;
prevDisplacement = displacement;
}
activateLine(prevPoint, dest, thickness);
}
public void DeactivateSegments()
{
for(int i = ActiveLineObj.Count - 1; i >= 0; i--)
{
GameObject line = ActiveLineObj[i];
line.SetActive(false);
ActiveLineObj.RemoveAt(i);
InactiveLineObj.Add(line);
}
}
void activateLine(Vector2 A, Vector2 B, float thickness)
{
//get the inactive count
int inactiveCount = InactiveLineObj.Count;
//only activate if we can pull from inactive
if(inactiveCount <= 0) return;
//pull the GameObject
GameObject line = InactiveLineObj[inactiveCount - 1];
//set it active
line.SetActive(true);
//get the Line component
Line lineComponent = line.GetComponent<Line>();
lineComponent.SetColor(Color.white);
lineComponent.A = A;
lineComponent.B = B;
lineComponent.Thickness = thickness;
InactiveLineObj.RemoveAt(inactiveCount - 1);
ActiveLineObj.Add(line);
}
public void Draw()
{
//if the bolt has faded out, no need to draw
if (Alpha <= 0) return;
foreach (GameObject obj in ActiveLineObj)
{
Line lineComponent = obj.GetComponent<Line>();
lineComponent.SetColor(Tint * (Alpha * 0.6f));
lineComponent.Draw();
}
}
public void Update()
{
Alpha -= FadeOutRate;
}
//...
}
The code may look a bit intimidating, but it's not so bad once you understand the logic. Before we continue on, understand that we've chosen to pool our line segments in the bolts (since constantly instantiating and destroying objects can be costly in Unity).
The Initialize() function will be called once on each lightning bolt and will determine how many line segments each bolt is allowed to use.
The activateLine() function will activate a line segment using the given position data.
The DeactivateSegments() function will deactivate any active line segments in our bolt.
The ActivateBolt() function will handle creating our jagged lines and will call the activateLine() function to activate our line segments at the appropriate positions.
To create our jagged lines, we start by computing the slope between our two points, as well as the normal vector to that slope. We then choose a number of random positions along the line and store them in our positions list. We scale these positions between 0 and 1, such that 0 represents the start of the line and 1 represents the end point., and then sort these positions to allow us to easily add line segments between them.
The loop goes through the randomly chosen points and displaces them along the normal by a random amount. The scale factor is there to avoid overly sharp angles, and the envelope ensures the lightning actually goes to the destination point by limiting displacement when we're close to the end. The spread is to assist in controlling how far the segments deviate from the slope of our line; a spread of 0 will essentially give you a straight line.
So, like we did with our Line class, let's make this into a prefab. From the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to Bolt, and drag a copy of the LightningBolt script onto it. Finally, click on the Bolt object and assign the Line prefab, from the Prefabs folder, to the appropriate slot in the LightningBolt script. Once you're done with that, simply drag the Bolt object into the Prefabs folder to create a prefab.
Step 3: Add Animation
Lightning should flash brightly and then fade out. This is what our Update() and Draw() functions in LightningBolt are for. Calling Update() will make the bolt fade out. Calling Draw() will update the bolt's color on the screen. IsComplete will tell you when the bolt has fully faded out.
Step 4: Create a Bolt
Now that we have our LightningBolt class, let's actually put it to good use and set up a quick demo scene.
We're going to use an object pool for this demo, so we'll want to create an empty object to hold our active and inactive bolts (simply for organizational purposes). In Unity, from the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to LightningPoolHolder.
Right click on the Scripts folder and select Create > C# Script. Name your script DemoScript and open it. Here's some quick code to get you started:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class DemoScript : MonoBehaviour
{
//Prefabs to be assigned in Editor
public GameObject BoltPrefab;
//For pooling
List<GameObject> activeBoltsObj;
List<GameObject> inactiveBoltsObj;
int maxBolts = 1000;
//For handling mouse clicks
int clicks = 0;
Vector2 pos1, pos2;
void Start()
{
//Initialize lists
activeBoltsObj = new List<GameObject>();
inactiveBoltsObj = new List<GameObject>();
//Grab the parent we'll be assigning to our bolt pool
GameObject p = GameObject.Find("LightningPoolHolder");
//For however many bolts we've specified
for(int i = 0; i < maxBolts; i++)
{
//create from our prefab
GameObject bolt = (GameObject)Instantiate(BoltPrefab);
//Assign parent
bolt.transform.parent = p.transform;
//Initialize our lightning with a preset number of max sexments
bolt.GetComponent<LightningBolt>().Initialize(25);
//Set inactive to start
bolt.SetActive(false);
//Store in our inactive list
inactiveBoltsObj.Add(bolt);
}
}
void Update()
{
//Declare variables for use later
GameObject boltObj;
LightningBolt boltComponent;
//store off the count for effeciency
int activeLineCount = activeBoltsObj.Count;
//loop through active lines (backwards because we'll be removing from the list)
for (int i = activeLineCount - 1; i >= 0; i--)
{
//pull GameObject
boltObj = activeBoltsObj[i];
//get the LightningBolt component
boltComponent = boltObj.GetComponent<LightningBolt>();
//if the bolt has faded out
if(boltComponent.IsComplete)
{
//deactive the segments it contains
boltComponent.DeactivateSegments();
//set it inactive
boltObj.SetActive(false);
//move it to the inactive list
activeBoltsObj.RemoveAt(i);
inactiveBoltsObj.Add(boltObj);
}
}
//If left mouse button pressed
if(Input.GetMouseButtonDown(0))
{
//if first click
if(clicks == 0)
{
//store starting position
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos1 = new Vector2(temp.x, temp.y);
}
else if(clicks == 1) //second click
{
//store end position
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos2 = new Vector2(temp.x, temp.y);
//create a (pooled) bolt from pos1 to pos2
CreatePooledBolt(pos1,pos2, Color.white, 1f);
}
//increment our tick count
clicks++;
//restart the count after 2 clicks
if(clicks > 1) clicks = 0;
}
//update and draw active bolts
for(int i = 0; i < activeBoltsObj.Count; i++)
{
activeBoltsObj[i].GetComponent<LightningBolt>().UpdateBolt();
activeBoltsObj[i].GetComponent<LightningBolt>().Draw();
}
}
void CreatePooledBolt(Vector2 source, Vector2 dest, Color color, float thickness)
{
//if there is an inactive bolt to pull from the pool
if(inactiveBoltsObj.Count > 0)
{
//pull the GameObject
GameObject boltObj = inactiveBoltsObj[inactiveBoltsObj.Count - 1];
//set it active
boltObj.SetActive(true);
//move it to the active list
activeBoltsObj.Add(boltObj);
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1);
//get the bolt component
LightningBolt boltComponent = boltObj.GetComponent<LightningBolt>();
//activate the bolt using the given position data
boltComponent.ActivateBolt(source, dest, color, thickness);
}
}
}
All this code does is give us a way to create bolts using object pooling. There are other ways you can set this up, but this is what we're going with! Once we've set it up, all you'll have to do is click twice to create a bolt on the screen: once for the start position and once for the end position.
We'll need an object to put our DemoScript on. From the menu, select GameObject > Create Empty. The object will appear in your Hierarchy panel. Rename it to DemoScript and drag your DemoScript script onto it. Click on the DemoScript object so we can view it in the Inspector panel. Assign the Bolt prefab, from the Prefabs folder, to the matching slot in the DemoScript.
That should be enough to get you going! Run the scene in Unity and try it out!
Step 5: Create Branch Lightning
You can use the LightningBolt class as a building block to create more interesting lightning effects. For example, you can make the bolts branch out as shown below:
To make the lightning branch, we pick random points along the lightning bolt and add new bolts that branch out from these points. In the code below, we create between three and six branches which separate from the main bolt at 30° angles.
using UnityEngine;
using System.Collections.Generic;
class BranchLightning : MonoBehaviour
{
//For holding all of our bolts in our branch
List<GameObject> boltsObj = new List<GameObject>();
//If there are no bolts, then the branch is complete (we're not pooling here, but you could if you wanted)
public bool IsComplete { get { return boltsObj.Count == 0; } }
//Start position of branch
public Vector2 Start { get; private set; }
//End position of branch
public Vector2 End { get; private set; }
static Random rand = new Random();
public void Initialize(Vector2 start, Vector2 end, GameObject boltPrefab)
{
//store start and end positions
Start = start;
End = end;
//create the main bolt from our bolt prefab
GameObject mainBoltObj = (GameObject)GameObject.Instantiate(boltPrefab);
//get the LightningBolt component
LightningBolt mainBoltComponent = mainBoltObj.GetComponent<LightningBolt>();
//initialize our bolt with a max of 5 segments
mainBoltComponent.Initialize(5);
//activate the bolt with our position data
mainBoltComponent.ActivateBolt(start, end, Color.white, 1f);
//add it to our list
boltsObj.Add(mainBoltObj);
//randomly determine how many sub branches there will be (3-6)
int numBranches = Random.Range(3,6);
//calculate the difference between our start and end points
Vector2 diff = end - start;
// pick a bunch of random points between 0 and 1 and sort them
List<float> branchPoints = new List<float>();
for(int i = 0; i < numBranches; i++) branchPoints.Add(Random.value);
branchPoints.Sort();
//go through those points
for (int i = 0; i < branchPoints.Count; i++)
{
// Bolt.GetPoint() gets the position of the lightning bolt based on the percentage passed in (0 = start of bolt, 1 = end)
Vector2 boltStart = mainBoltComponent.GetPoint(branchPoints[i]);
//get rotation of 30 degrees. Alternate between rotating left and right. (i & 1 will be true for all odd numbers...yay bitwise operators!)
Quaternion rot = Quaternion.AngleAxis(30 * ((i & 1) == 0 ? 1 : -1), new Vector3(0,0,1));
//calculate how much to adjust for our end position
Vector2 adjust = rot * (Random.Range(.5f, .75f) * diff * (1 - branchPoints[i]));
//get the end position
Vector2 boltEnd = adjust + boltStart;
//instantiate from our bolt prefab
GameObject boltObj = (GameObject)GameObject.Instantiate(boltPrefab);
//get the LightningBolt component
LightningBolt boltComponent = boltObj.GetComponent<LightningBolt>();
//initialize our bolt with a max of 5 segments
boltComponent.Initialize(5);
//activate the bolt with our position data
boltComponent.ActivateBolt(boltStart, boltEnd, Color.white, 1f);
//add it to the list
boltsObj.Add(boltObj);
}
}
public void UpdateBranch()
{
//go through our active bolts
for (int i = boltsObj.Count - 1; i >= 0; i--)
{
//get the GameObject
GameObject boltObj = boltsObj[i];
//get the LightningBolt component
LightningBolt boltComp = boltObj.GetComponent<LightningBolt>();
//update/fade out the bolt
boltComp.UpdateBolt();
//if the bolt has faded
if(boltComp.IsComplete)
{
//remove it from our list
boltsObj.RemoveAt(i);
//destroy it (would be better to pool but I'll let you figure out how to do that =P)
Destroy(boltObj);
}
}
}
//Draw our active bolts on screen
public void Draw()
{
foreach (GameObject boltObj in boltsObj)
{
boltObj.GetComponent<LightningBolt>().Draw();
}
}
}
This code works very similarly to our LightningBolt class with the exception that it does not use object pooling. Calling Initialize() is all you will need to do to create a branching bolt; after that, you will just need to call Update() and Draw(). I'll show you exactly how to do this in our DemoScript later on in the tutorial.
You may have noticed the reference to a GetPoint() function in the LightningBolt class. We haven't actually implemented that function yet, so let's take care of that now.
Add the following function in the bottom of the LightningBolt class:
// Returns the point where the bolt is at a given fraction of the way through the bolt. Passing
// zero will return the start of the bolt, and passing 1 will return the end.
public Vector2 GetPoint(float position)
{
Vector2 start = Start;
float length = Vector2.Distance(start, End);
Vector2 dir = (End - start) / length;
position *= length;
//find the appropriate line
Line line = ActiveLineObj.Find(x => Vector2.Dot(x.GetComponent<Line>().B - start, dir) >= position).GetComponent<Line>();
float lineStartPos = Vector2.Dot(line.A - start, dir);
float lineEndPos = Vector2.Dot(line.B - start, dir);
float linePos = (position - lineStartPos) / (lineEndPos - lineStartPos);
return Vector2.Lerp(line.A, line.B, linePos);
}
Step 6: Create Lightning Text
Below is a video of another effect you can make out of the lightning bolts:
We'll need to do a little more setup for this one. First, from the Project panel, select Create > RenderTexture. Rename it to RenderText and set its Size to 256x256px. (It doesn't necessarily have to be that exact size, but the smaller it is, the faster the program will run.)
From the menu, select Edit > Project Settings > Tags and Layers. Then, in the Inspector panel, expand the Layers drop down and add Text into User Layer 8.
We'll then need to create a second camera. From the menu, select GameObject > Create Other > Camera. Rename it to TextCamera, and set its Projection to Orthographic and its Clear Flags to Solid Color. Set its Background color to (R: 0, G: 0, B: 0, A: 0) and set its Culling Mask to only be Text (the layer we just created). Finally, set its Target Texture to RenderText (the RenderTexture we created earlier). You'll probably need to play around with the camera's Size later, in order to get everything to fit on the screen.
Now we'll need to create the actual text we'll be drawing with our lightning. From the menu select GameObject > Create Other > GUI Text. Select the GUI Text object from the Hierarchy panel and set its Text to LIGHTNING, its Anchor to middle center, and its Alignment to center. Then, set its Layer to the Text layer we created earlier. You'll probably have to play around with the Font Size in order to fit the text on the screen.
Now select the Main Camera and set its Culling Mask to be everything but our Text layer. This will cause our GUI Text to apparently disappear from the screen, but it should be drawn on the RenderTexture we created earlier: select RenderText from the Project panel and you should be able to see the word LIGHTNING on the preview on the bottom of the panel.
If you can't see the word LIGHTNING, you'll need to play around with your positioning, font size, and (text) camera size. To help you position your text, click on TextCamera in the Hierarchy panel, and set its Target Texture to None. You'll now be able to see your GUI Text if you center it on the TextCamera. Once you have everything positioned, set the TextCamera's Target Texture back to RenderText.
Now for the code! We'll need to get the pixels from the text that we're drawing. We can do this by drawing our text to a RenderTarget and reading back the pixel data into a Texture2D with Texture2D.ReadPixels(). Then, we can store the coordinates of the pixels from the text as a List<Vector2>.
Here's the code to do that:
//Capture the important points of our text for later
IEnumerator TextCapture()
{
//must wait until end of frame so something is actually drawn or else it will error
yield return new WaitForEndOfFrame();
//get the camera that draws our text
Camera cam = GameObject.Find("TextCamera").GetComponent<Camera>();
//make sure it has an assigned RenderTexture
if(cam.targetTexture != null)
{
//pull the active RenderTexture
RenderTexture.active = cam.targetTexture;
//capture the image into a Texture2D
Texture2D image = new Texture2D(cam.targetTexture.width, cam.targetTexture.height);
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0);
image.Apply();
//calculate how the text will be scaled when it is displayed as lightning on the screen
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x);
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
positionText.x -= image.width * scaleText * .5f;
positionText.y -= image.height * scaleText * .5f;
//basically determines how many pixels we skip/check
const int interval = 2;
//loop through pixels
for(int y = 0; y < image.height; y += interval)
{
for(int x = 0; x < image.width; x += interval)
{
//get the color of the pixel
Color color = image.GetPixel(x,y);
//if the color has any r (red) value
if(color.r > 0)
{
//add it to our points for drawing
textPoints.Add(new Vector2(x,y));
}
}
}
}
}
Note: We'll have to run this function as a Coroutine at the start of our program in order for it to run correctly.
After that, each frame, we can randomly pick pairs of these points and create a lightning bolt between them. We want to design it so that the closer two points are to one another, the greater the chance is that we create a bolt between them.
There's a simple technique we can use to accomplish this: we'll pick the first point at random, and then we'll pick a fixed number of other points at random and choose the nearest.
Here's the code for that (we'll add it to our DemoScript later):
//go through the points we capture earlier
foreach (Vector2 point in textPoints)
{
//randomly ignore certain points
if(Random.Range(0,75) != 0) continue;
//placeholder values
Vector2 nearestParticle = Vector2.zero;
float nearestDistSquared = float.MaxValue;
for (int i = 0; i < 50; i++)
{
//select a random point
Vector2 other = textPoints[Random.Range(0, textPoints.Count)];
//calculate the distance (squared for performance benefits) between the two points
float distSquared = DistanceSquared(point, other);
//If this point is the nearest point (but not too near!)
if (distSquared < nearestDistSquared && distSquared > 3 * 3)
{
//store off the data
nearestDistSquared = distSquared;
nearestParticle = other;
}
}
//if the point we found isn't too near/far
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3)
{
//create a (pooled) bolt at the corresponding screen position
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f);
}
}
/* The code above uses the following function
* It'll need to be placed appropriately
---------------------------------------------
//calculate distance squared (no square root = performance boost)
public float DistanceSquared(Vector2 a, Vector2 b)
{
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
---------------------------------------------*/
The number of candidate points we test will affect the look of the lightning text; checking a larger number of points will allow us to find very close points to draw bolts between, which will make the text very neat and legible, but with fewer long lightning bolts between letters. Smaller numbers will make the lightning text look wilder but less legible.
Step 7: Try Other Variations
We've discussed making branch lightning and lightning text, but those certainly aren't the only effects you can make. Let's look at a couple other variations on lightning you may want to use.
Moving Lightning
You may often want to make a moving bolt of lightning. You can do this by adding a new short bolt each frame at the end point of the previous frame's bolt.
//Will contain all of the pieces for the moving bolt
List<GameObject> movingBolt = new List<GameObject>();
//used for actually moving the moving bolt
Vector2 lightningEnd = new Vector2(100, 100);
Vector2 lightningVelocity = new Vector2(1, 0);
void Update()
{
//loop through all of our bolts that make up the moving bolt
for(int i = movingBolt.Count - 1; i >= 0; i--)
{
//get the bolt component
boltComponent = movingBolt[i].GetComponent<LightningBolt>();
//if the bolt has faded out
if(boltComponent.IsComplete)
{
//destroy it
Destroy(movingBolt[i]);
//remove it from our list
movingBolt.RemoveAt(i);
//on to the next one, on on to the next one
continue;
}
//update and draw bolt
boltComponent.UpdateBolt();
boltComponent.Draw();
}
//if our moving bolt is active
if(movingBolt.Count > 0)
{
//calculate where it currently ends
lightningEnd = movingBolt[movingBolt.Count-1].GetComponent<LightningBolt>().End;
//if the end of the bolt is within 25 units of the camera
if(Vector2.Distance(lightningEnd,(Vector2)Camera.main.transform.position) < 25)
{
//instantiate from our bolt prefab
boltObj = (GameObject)GameObject.Instantiate(BoltPrefab);
//get the bolt component
boltComponent = boltObj.GetComponent<LightningBolt>();
//initialize it with a maximum of 5 segments
boltComponent.Initialize(5);
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f);
//add it to our list
movingBolt.Add(boltObj);
//update and draw our new bolt
boltComponent.UpdateBolt();
boltComponent.Draw();
}
}
}
Burst Lightning
This variation offers a dramatic effect that shoots lightning out in a circle from the centre point:
Vector2 diff = pos2 - pos1;
void Update()
{
//define how many bolts we want in our circle
int boltsInBurst = 10;
for(int i = 0; i < boltsInBurst; i++)
{
//rotate around the z axis to the appropriate angle
Quaternion rot = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1));
//Calculate the end position for the bolt
Vector2 boltEnd = (Vector2)(rot * diff) + pos1;
//create a (pooled) bolt from pos1 to boltEnd
CreatePooledBolt(pos1, boltEnd, Color.white, 1f);
}
}
Step 8: Put It All Together in DemoScript
You're going to want to be able to try out all of these fancy effects we've created so far, so let's put all of them into the DemoScript we made earlier. You'll be able to toggle between effects by hitting the number keys on your keyboard to select the effect, and then just clicking twice like we did with our bolts before.
Here's the full code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class DemoScript : MonoBehaviour
{
//Prefabs to be assigned in Editor
public GameObject BoltPrefab;
public GameObject BranchPrefab;
//For pooling
List<GameObject> activeBoltsObj;
List<GameObject> inactiveBoltsObj;
int maxBolts = 1000;
float scaleText;
Vector2 positionText;
//Different modes for the demo
enum Mode : byte
{
bolt,
branch,
moving,
text,
nodes,
burst
}
//The current mode the demo is in
Mode currentMode = Mode.bolt;
//Will contain all of the pieces for the moving bolt
List<GameObject> movingBolt = new List<GameObject>();
//used for actually moving the moving bolt
Vector2 lightningEnd = new Vector2(100, 100);
Vector2 lightningVelocity = new Vector2(1, 0);
//Will contain all of the pieces for the branches
List<GameObject> branchesObj;
//For handling mouse clicks
int clicks = 0;
Vector2 pos1, pos2;
//For storing all of the pixels that need to be drawn by the bolts
List<Vector2> textPoints = new List<Vector2>();
//true in text mode
bool shouldText = false;
void Start()
{
//Initialize lists
activeBoltsObj = new List<GameObject>();
inactiveBoltsObj = new List<GameObject>();
branchesObj = new List<GameObject>();
//Grab the parent we'll be assigning to our bolt pool
GameObject p = GameObject.Find("LightningPoolHolder");
//For however many bolts we've specified
for(int i = 0; i < maxBolts; i++)
{
//create from our prefab
GameObject bolt = (GameObject)Instantiate(BoltPrefab);
//Assign parent
bolt.transform.parent = p.transform;
//Initialize our lightning with a preset number of max sexments
bolt.GetComponent<LightningBolt>().Initialize(25);
//Set inactive to start
bolt.SetActive(false);
//Store in our inactive list
inactiveBoltsObj.Add(bolt);
}
//Start up a coroutine to capture the pixels we'll be drawing from our text (need the coroutine or error)
StartCoroutine(TextCapture());
}
void Update()
{
//Declare variables for use later
GameObject boltObj;
LightningBolt boltComponent;
//store off the count for effeciency
int activeLineCount = activeBoltsObj.Count;
//loop through active lines (backwards because we'll be removing from the list)
for (int i = activeLineCount - 1; i >= 0; i--)
{
//pull GameObject
boltObj = activeBoltsObj[i];
//get the LightningBolt component
boltComponent = boltObj.GetComponent<LightningBolt>();
//if the bolt has faded out
if(boltComponent.IsComplete)
{
//deactive the segments it contains
boltComponent.DeactivateSegments();
//set it inactive
boltObj.SetActive(false);
//move it to the inactive list
activeBoltsObj.RemoveAt(i);
inactiveBoltsObj.Add(boltObj);
}
}
//check for key press and set mode accordingly
if(Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
shouldText = false;
currentMode = Mode.bolt;
}
else if(Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
shouldText = false;
currentMode = Mode.branch;
}
else if(Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
shouldText = false;
currentMode = Mode.moving;
}
else if(Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
{
shouldText = true;
currentMode = Mode.text;
}
else if(Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
{
shouldText = false;
currentMode = Mode.nodes;
}
else if(Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6))
{
shouldText = false;
currentMode = Mode.burst;
}
//If left mouse button pressed
if(Input.GetMouseButtonDown(0))
{
//if first click
if(clicks == 0)
{
//store starting position
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos1 = new Vector2(temp.x, temp.y);
}
else if(clicks == 1) //second click
{
//store end position
Vector3 temp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
pos2 = new Vector2(temp.x, temp.y);
//Handle the current mode appropriately
switch (currentMode)
{
case Mode.bolt:
//create a (pooled) bolt from pos1 to pos2
CreatePooledBolt(pos1,pos2, Color.white, 1f);
break;
case Mode.branch:
//instantiate from our branch prefab
GameObject branchObj = (GameObject)GameObject.Instantiate(BranchPrefab);
//get the branch component
BranchLightning branchComponent = branchObj.GetComponent<BranchLightning>();
//initialize the branch component using our position data
branchComponent.Initialize(pos1, pos2, BoltPrefab);
//add it to the list of active branches
branchesObj.Add(branchObj);
break;
case Mode.moving:
//Prevent from getting a 0 magnitude (0 causes errors
if(Vector2.Distance(pos1, pos2) <= 0)
{
//Try a random position
Vector2 adjust = Random.insideUnitCircle;
//failsafe
if(adjust.magnitude <= 0) adjust.x += .1f;
//Adjust the end position
pos2 += adjust;
}
//Clear out any old moving bolt (this is designed for one moving bolt at a time)
for(int i = movingBolt.Count - 1; i >= 0; i--)
{
Destroy(movingBolt[i]);
movingBolt.RemoveAt(i);
}
//get the "velocity" so we know what direction to send the bolt in after initial creation
lightningVelocity = (pos2 - pos1).normalized;
//instantiate from our bolt prefab
boltObj = (GameObject)GameObject.Instantiate(BoltPrefab);
//get the bolt component
boltComponent = boltObj.GetComponent<LightningBolt>();
//initialize it with 5 max segments
boltComponent.Initialize(5);
//activate the bolt using our position data
boltComponent.ActivateBolt(pos1, pos2, Color.white, 1f);
//add it to our list
movingBolt.Add(boltObj);
break;
case Mode.burst:
//get the difference between our two positions (destination - source = vector from source to destination)
Vector2 diff = pos2 - pos1;
//define how many bolts we want in our circle
int boltsInBurst = 10;
for(int i = 0; i < boltsInBurst; i++)
{
//rotate around the z axis to the appropriate angle
Quaternion rot = Quaternion.AngleAxis((360f/boltsInBurst) * i, new Vector3(0,0,1));
//Calculate the end position for the bolt
Vector2 boltEnd = (Vector2)(rot * diff) + pos1;
//create a (pooled) bolt from pos1 to boltEnd
CreatePooledBolt(pos1, boltEnd, Color.white, 1f);
}
break;
}
}
//increment our tick count
clicks++;
//restart the count after 2 clicks
if(clicks > 1) clicks = 0;
}
//if in node mode
if(currentMode == Mode.nodes)
{
//constantly create a (pooled) bolt between the two assigned positions
CreatePooledBolt(pos1, pos2, Color.white, 1f);
}
//loop through any active branches
for(int i = branchesObj.Count - 1; i >= 0; i--)
{
//pull the branch lightning component
BranchLightning branchComponent = branchesObj[i].GetComponent<BranchLightning>();
//If it's faded out already
if(branchComponent.IsComplete)
{
//destroy it
Destroy(branchesObj[i]);
//take it out of our list
branchesObj.RemoveAt(i);
//move on to the next branch
continue;
}
//draw and update the branch
branchComponent.UpdateBranch();
branchComponent.Draw();
}
//loop through all of our bolts that make up the moving bolt
for(int i = movingBolt.Count - 1; i >= 0; i--)
{
//get the bolt component
boltComponent = movingBolt[i].GetComponent<LightningBolt>();
//if the bolt has faded out
if(boltComponent.IsComplete)
{
//destroy it
Destroy(movingBolt[i]);
//remove it from our list
movingBolt.RemoveAt(i);
//on to the next one, on on to the next one
continue;
}
//update and draw bolt
boltComponent.UpdateBolt();
boltComponent.Draw();
}
//if our moving bolt is active
if(movingBolt.Count > 0)
{
//calculate where it currently ends
lightningEnd = movingBolt[movingBolt.Count-1].GetComponent<LightningBolt>().End;
//if the end of the bolt is within 25 units of the camera
if(Vector2.Distance(lightningEnd,(Vector2)Camera.main.transform.position) < 25)
{
//instantiate from our bolt prefab
boltObj = (GameObject)GameObject.Instantiate(BoltPrefab);
//get the bolt component
boltComponent = boltObj.GetComponent<LightningBolt>();
//initialize it with a maximum of 5 segments
boltComponent.Initialize(5);
//activate the bolt using our position data (from the current end of our moving bolt to the current end + velocity)
boltComponent.ActivateBolt(lightningEnd,lightningEnd + lightningVelocity, Color.white, 1f);
//add it to our list
movingBolt.Add(boltObj);
//update and draw our new bolt
boltComponent.UpdateBolt();
boltComponent.Draw();
}
}
//if in text mode
if(shouldText)
{
//go through the points we capture earlier
foreach (Vector2 point in textPoints)
{
//randomly ignore certain points
if(Random.Range(0,75) != 0) continue;
//placeholder values
Vector2 nearestParticle = Vector2.zero;
float nearestDistSquared = float.MaxValue;
for (int i = 0; i < 50; i++)
{
//select a random point
Vector2 other = textPoints[Random.Range(0, textPoints.Count)];
//calculate the distance (squared for performance benefits) between the two points
float distSquared = DistanceSquared(point, other);
//If this point is the nearest point (but not too near!)
if (distSquared < nearestDistSquared && distSquared > 3 * 3)
{
//store off the data
nearestDistSquared = distSquared;
nearestParticle = other;
}
}
//if the point we found isn't too near/far
if (nearestDistSquared < 25 * 25 && nearestDistSquared > 3 * 3)
{
//create a (pooled) bolt at the corresponding screen position
CreatePooledBolt((point * scaleText) + positionText, (nearestParticle * scaleText) + positionText, new Color(Random.value,Random.value,Random.value,1f), 1f);
}
}
}
//update and draw active bolts
for(int i = 0; i < activeBoltsObj.Count; i++)
{
activeBoltsObj[i].GetComponent<LightningBolt>().UpdateBolt();
activeBoltsObj[i].GetComponent<LightningBolt>().Draw();
}
}
//calculate distance squared (no square root = performance boost)
public float DistanceSquared(Vector2 a, Vector2 b)
{
return ((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
void CreatePooledBolt(Vector2 source, Vector2 dest, Color color, float thickness)
{
//if there is an inactive bolt to pull from the pool
if(inactiveBoltsObj.Count > 0)
{
//pull the GameObject
GameObject boltObj = inactiveBoltsObj[inactiveBoltsObj.Count - 1];
//set it active
boltObj.SetActive(true);
//move it to the active list
activeBoltsObj.Add(boltObj);
inactiveBoltsObj.RemoveAt(inactiveBoltsObj.Count - 1);
//get the bolt component
LightningBolt boltComponent = boltObj.GetComponent<LightningBolt>();
//activate the bolt using the given position data
boltComponent.ActivateBolt(source, dest, color, thickness);
}
}
//Capture the important points of our text for later
IEnumerator TextCapture()
{
//must wait until end of frame so something is actually drawn or else it will error
yield return new WaitForEndOfFrame();
//get the camera that draws our text
Camera cam = GameObject.Find("TextCamera").GetComponent<Camera>();
//make sure it has an assigned RenderTexture
if(cam.targetTexture != null)
{
//pull the active RenderTexture
RenderTexture.active = cam.targetTexture;
//capture the image into a Texture2D
Texture2D image = new Texture2D(cam.targetTexture.width, cam.targetTexture.height);
image.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0);
image.Apply();
//calculate how the text will be scaled when it is displayed as lightning on the screen
scaleText = 1 / (cam.ViewportToWorldPoint(new Vector3(1,0,0)).x - cam.ViewportToWorldPoint(Vector3.zero).x);
//calculate how the text will be positioned when it is displayed as lightning on the screen (centered)
positionText.x -= image.width * scaleText * .5f;
positionText.y -= image.height * scaleText * .5f;
//basically determines how many pixels we skip/check
const int interval = 2;
//loop through pixels
for(int y = 0; y < image.height; y += interval)
{
for(int x = 0; x < image.width; x += interval)
{
//get the color of the pixel
Color color = image.GetPixel(x,y);
//if the color has any r (red) value
if(color.r > 0)
{
//add it to our points for drawing
textPoints.Add(new Vector2(x,y));
}
}
}
}
}
}
Conclusion
Lightning is a great special effect for sprucing up your games. The effects described in this tutorial are a nice starting point, but it's certainly not all you can do with lightning. With a bit of imagination you can make all kinds of awe-inspiring lightning effects! Download the source code and experiment on your own.
You have until September 30th to grab a student subscription at our special price of just $45! Thanks to our friends at PayPal, we’re pleased to bring you this fantastic discount to help you with your studies.
With over 400 video courses in a range of topics, we know you’ll find something to help you with your project. Learn a new skill, brush up on an old one, or discover a new hobby across a range of different categories:
Photography
Code
Web Design
Design & Illustration
Here's a quick example from one of our recent courses on photography:
Our Tuts+ Student Subscription entitles you to unlimited course access, removes ads from all our tutorials, and gives you five eBook downloads per month. All this, plus access to our special subscriber benefits and discounts.
To be eligible for this amazing offer, you must have a valid student email ending in .edu or .ac, or upload a picture of your current student ID.
Imagine if you designed your games to be interesting, rather than fun. That might sound a little counter-intuitive; you might say, "if a game is fun, wouldn't that already make it interesting, in some respect"? That's true, but I'm talking about giving the player a drive to play the game outside of simply completing the goals you have set. The player also plays to experience and explore the game itself, to see what the world you have created has to offer.
This has far more retaining power than straight up fun, and is amazing (thanks to the huge synergy bonus) when combined with fun.
It's hard to get a grasp on the fuzzy concept of "interesting", so rather than try to define it, let's look at six great examples of games that are "interesting", and at how they used "interest" to drive and support the game.
Note: This article contains spoilers for the following games:
If you haven't played them yet, I recommend you do so before reading on; they're all excellent experiences that deserve to be played through with fresh eyes—and they have plenty of lessons to teach any game designer.
The Stanely Parable, on paper, is not a "fun" game. You walk through hallways, you get talked at the whole time, and you get different endings based on whether you walk down one hallway or another. Then you play again and walk down the hallway you didn't walk down before. Sounds boring.
However, the designers used a powerful tool to keep the player interested in the game: player agency curiosity. It makes all the difference.
Player Agency Curiosity
This refers to the state where the player is engaged with the game by poking and prodding it—sometimes in hope of breaking the game, sometimes to see how it reacts, sometimes to find the limits or seams of the system, and so on.
When trying to harness this style of engagement, you must be able to predict what the player will try to do and what they are thinking. When they try to do something, your game must react to it, and these reactions must also provoke some sort of reaction from the player: fear, laughter, mystery, a clue, or even just a little Easter egg.
The Stanley Parable makes amazing use of this. The design tightly restricts the possible interactions for the player, and so the designers are able to predict what the player will do at any point. Because of this, they are able to be proactive in their reactions to player choices.
The best example of this is in the iconic two door room the player is presented with at the start of the game. The game tries to tell you the "story" of the game by saying "Stanley, when presented with two doors, walked through the left door." A player might say "Aha—nice try, game, but I won't let you control me!" and walk through the right door. However, the game recognizes this mindset and both acknowledges and responds to this by telling the player that they aren't following the proper story.
It's a small thing, but it makes the player want to see how far they can go: if I do this, what will happen? If I do that, is anything going to happen?
Frog Fractions is an educational game about fractions... sort of. It doesn't look like anything you haven't seen before. The gameplay is predictable; you know what you're getting as soon as you start playing it. You're a frog that needs to eat some bugs. Simple. But the game knows this, and it uses whatever assumptions you may have made about its gameplay against you in a brilliant way, through its unfolding gameplay.
Unfolding Gameplay
This is the act of constantly introducing new concepts and mechanics into your game the farther into your game the player gets. Games that use this device generally start out boring or mundane or predictable, but they need to. This is because when the player is in that state of boredom, they will perk up to anything new, and anything unusual.
Once the player starts to see the game expanding, they begin to wonder what else it has to show them. They want to know where this rabbit hole of a game leads. And since the game starts out as mundane, stale, and boring, when more of the game is revealed it's a big juxtaposition against what they were playing previously. This allows mediocre gameplay to become so much more, because each new gameplay element is new, shiny, and interesting.
In Frog Fractions, this first instance of unfolding generally happens at the perfect moment. The gameplay mechanics seem to boil down to eat some bugs, catch some fruit, unlock new power-ups so you can be more efficient at the process. It's nothing new and is easy to understand (aside, perhaps, from the quirky humor the game has).
However, that idea of the player having a full grasp on what the game is is quickly turned on its head as soon as the player moves downward just a little too far and bam. There are infinitely many pieces of fruit underwater! This immediately tells the player, "this game isn't about upgrading; it isn't about what you thought it was." Through this, the game creates a new rush of interest because now the game is broken—or is it? What is this game trying to do? The player continues to play to answer these questions.
Shadow of the Colossus has a lot going on under the hood. Its a game about traversing over expansive lands that is home to the colossi (giant enemy beasts). The game doesn't have a lot of conflict; there are only 16 enemies in the whole game. For each enemy, you must do the same thing: travel to the colossus, figure out how to get on top of the beast, find its weak points, kill, repeat.
Now, while that is simple when written, the game and how it was pieced together makes it insanely compelling because of differences in kind.
Differences in Kind
This is a term I picked up from the lovely Extra Credits team. It refers to the concept of a game changing tones throughout, in order to break up similar gameplay, to make sure that the player doesn't get fatigued from the gameplay—to give them a break of sorts. It is also used to make certain aspects of the game more impactful. Action, for instance, is much more refreshing when you are able to take a small break instead of having a constant barrage of endless fights until the game ends.
Shadow of the Colossus uses differences in kind beautifully. Since each of the battles with the colossi are intense puzzle/action segments, the player would quickly become tired if they were constantly playing this segment of the game. To prevent this, the fights are separated by calming, meditative traveling segments. These allow for the player to take a break from the action while also creating a sense of anticipation, adding additional value to each of the fights.
Differences in kind, when used properly, can also create different "lenses" through which to view the game itself. In Shadow of the Colossus, you view the game through both the lenses of action and calm. It's a hard feeling to describe, but a game that can be viewed through multiple lenses feels fuller, like more of a complete world. Games that have one lens tend to be games that feel more traditionally game-like.
While it's no bad thing to not have differences in kind, just know that it is a tool in your arsenal that won't (in most cases) diminish, only create additional value.
Corrypt is a weird game. It starts out normal enough: it's a down-to-earth Sokoban box game. Nothing special about that. But once you get far enough, you obtain magic. However, this magic actually ends up breaking the game, in some ways making it unplayable. This gives the player repercussions for agency.
Repercussions for Agency
What made this game absolutely brilliant for me was how it used a simple mechanic to expand the game in ways I didn't even know were possible before playing. You can use magic in order to permanently freeze a tile; when you go to any other room that tile will stay as what it was when you froze it.
This made me personally care about the repercussions of what I decided to do as a player. That is extremely important: the player themselves is invested in the decisions they make, but not through a binary "kill him or save him" situation where the outcome changes one ending to a different ending. No—this game made me stop and think, "if I freeze this tile what will happen? Is this smart? What rooms are going to be impossible to solve because of this?"
While having the decisions themselves be important to the player is significant, nothing is more essential than having those decisions impact the gameplay itself. In this game, the player's choices can change the layout of every room from then on. However, the effects doesn't always have to be as prominent. For example, in that hypothetical "kill him or save him" scenario, perhaps the player's character could be mentally scarred if they were to choose to kill, which in turn could make their accuracy go down, giving the choice a meaningful gameplay repercussion. If the player chose to save him, he could become a merchant that sells you some valuable items. These repercussions are simple, but far more meaningful to a player than a selection of two cut scenes.
In Papers, Please, you play as a border control worker who spends their days checking passports for authenticity. That is the game. It would have probably been completely mundane as well, if the developer hadn't put such care into making sure the game was constantly evolving, thus requiring perpetual mastery of the system from the player.
Perpetual Mastery of the System
This is when a game constantly changes how it is played. What makes this different from unfolding gameplay is that the genre and style of the game never changes. The actual changes are small—little additions to already established mechanics that make the player use what they've learned and apply it to different situations. Although they have mastered the actual mechanic itself, by being used in different situations the application of that mastered knowledge will change.
This isn't a new concept. Nearly any decent game uses this concept: Mario, Dark Souls, and The Last of Us are all good examples. However, this isn't something that every developer grasps. Artificial difficulty by way of increasing numbers does not create perpetual mastery; in these cases, the player has mastered the system but just has to continue executing what they have learned. This quickly becomes boring, and is a large part of why many games fail.
In Papers, Please the game requires perpetual mastery by changing how documents work. There's different paperwork for different countries, different occupations, and so on. As soon as the player has a grasp on the most recent twist on the mechanic, something new is thrown in. On top of that, there are a few special characters thrown in to make sure that the player never falls into a routine and can easily "game" the game.
Calm Time is a horror game with a unique twist. It isn't scary, in a normal sense. It's slow paced, twisted, and methodical. It places the player into the shoes of what would normally be the object of fear: a killer who has gathered their victims under the pretense of a dinner party. Throughout the game, you must kill each guest one by one, as they plead for their lives and run to live maybe just a second longer. It's a great example of forced perspective storytelling.
Forced Perspective Storytelling
This concept refers to when the game forces you, as the player, to tell the story of the game through the mechanics. This means that the mechanics and the player's use of them places the player in a certain mindset. Their goals align with the character's goals.
In Calm Time this becomes unsettling—not because of the actions of the character, but because of the goals of the player. The player's goal is to kill all the guests they have invited to their home. As the game goes on, it might become fun, tedious, or just a mindless action to complete the game. But through this we also come to completely embody the mindset of the character. The character becomes a mirror onto ourselves. He is clearly insane because he find this act of murder fun, tedious, or just a mindless action.
This is different to roleplaying, since instead of us choosing our actions based on the character we want to be, our character is defined by the actions we take and our reasons for taking said actions.
Fun Without Purpose
There are plenty of games that feel like they should be fun on paper, but aren't when actually played. We see this a lot in clones, which end up copying the "fun" gameplay, without much else of what made the original interesting. The game has the same ideas, but the execution falls flat. Why is that?
It's because these games mostly copy the games on a mechanical level, but they fail to understand how those mechanics were used in the games to make them compelling to play.
If you are going to try to remake a game, you must not only recreate the mechanics, you must also recreate the design. Games do not exist as a jumble of mechanics thrown together to make a game. Design is what gives the mechanics meaning, gives the player purpose, and creates the reason for the player to play your game.
Conclusion
Games can be fun, don't get me wrong, but it is important to remember that that's not all games can be. Games can be compelling in other ways, through use of intelligent design. Games can be interesting without having to overstimulate the player with explosions and the like.
There are many ways to go about this; I have only listed but a fraction of what is possible in games. Don't restrict yourself by thinking, "people won't play my game if it isn't fun." If your game is interesting, people will enjoy it.
Game jams are a great way to get some game development experience, meet other developers, and, ever importantly, have some fun. No game jams in your area? No problem! This simple guide will have you running your own game jam in no time.
What Is This "Game Jam" You Speak Of?
Simply put, a game jam is an event where people get together for a set period of time with the goal of creating a playable game. The details are open to a lot of creative variation: different lengths of time, online vs. physically located, etc.
The most typical format that I'm personally familiar with is the 48-hour-long game jam that takes place in a physical location, over a weekend, from Friday evening until Sunday evening. This is the format of OrcaJam, the game jam that I've been helping run for the past few years in Victoria, BC, Canada, and it is the format that I'll be discussing here—but feel free to consider other variations that may fit your own needs more closely.
To Jam or Not to Jam?
Maybe you're considering starting up your own local game jam but you're wondering whether it will be worth all of your time and effort (and don't get me wrong, it will require some time and effort to pull it off successfully). Allow me to convince you. Here is a quick list of some of the potential benefits that come from a game jam:
You're free to make a game without outside restrictions.
It provides networking opportunities with other developers.
You can find potential collaborators for projects.
You'll practice your game making skills.
You can try out a different game creation role (artist, designer, programmer).
You'll get feedback on your game.
You can see how other gamedevs make games.
It's an opportunity to socialize.
It's an excuse to stay up really late.
You'll have some fun!
Did I mention fun? Making games is a ton of fun. Whether you're a novice game creator who's wondering what all the fuss is about, or a seasoned veteran who's been making games all her life, making games can be enormously enjoyable. And making games at a game jam can be even more enjoyable, because you're doing it for the sheer joy of it.
In a game jam, there's no pressure to make something that will sell a million copies or even make a lot of sense to the average person. It's a great way to get back in touch with making games for fun, and to attempt those wacky projects that you wouldn't normally think twice about.
All right, you're probably thinking to yourself, that's great for attendees—but what do I get out of it as an organizer?
That's a good question. As the game jam organizer, you not only get to experience the benefits that everyone else does (as listed above), you also get some other sweet perks. For one thing, you get the satisfaction of giving back to your local game community.
If that's not enough, you also get a chance to experiment with the flavor and results of your game jam by setting the theme and activities within the jam itself. Witnessing the games created at the end of the jam is a pretty fantastic experience. Witnessing the resulting games created from the jam that you organized yourself is even better.
When to Host Your Game Jam
Now that you're keen on starting a game jam, the first question that you should ask is: When should I run this crazy thing? You might be all keen to jump in and announce your game jam for next weekend, but that would be unwise.
You'll need to give yourself enough time, not just to plan and organize all the details, but also to get the word out to potential participants so that they can dedicate a weekend to making games (which is not always easy, if you have family and other responsibilities to juggle). I recommend giving people at least a month's notice, and giving yourself even longer to work out the plan of attack.
Since game jams are typically indoor activities, you can easily book your jam for any season of the year. But keep in mind certain special dates and book around them to ensure good attendance:
Widely celebrated holidays
Dates of other popular game jams
Dates of major game conferences
Dates of major sporting events (Don't laugh. Try booking hotels when home games are on.)
For OrcaJam, we've booked weekends in August and September close to PAX Prime to get indies from out of town to drop by for our jam. Victoria is super-close to Seattle, so it's easy to get here from there.
Location, Location, Location
Now that you have your date worked out, you'll probably be asking: where should I put the darn thing? Finding the right space for a game jam can be tricky. Some things to consider when choosing a space: the number of potential attendees, your event budget, 24-hour accessibility, and so on.
One approach is to partner with educational institutions such as community colleges, universities, or trade schools. Look for ones that include game-related courses—computer science, art, game design—and approach those faculties directly, asking them to donate space to hold a game jam. They’re often willing to do so, but may have requirements for you to meet. For example, you may be required to hire extra security guards if you want the space to be accessible during off hours.
Other organizations that you might look at include not-for-profit organizations with a focus on technology, art, or business. For OrcaJam, we've partnered with a local outfit called the Victoria Advanced Technology Council (VIATeC) who have an interest in promoting the local technology sector. They've donated their space for our jams and helped promote it in the area. These organizations are usually easy to find with a bit of online searching or by talking with folks in the industry.
If you don't have any willing organizations nearby to partner with, hotels can be a good choice for game jams. They often have meeting or conference rooms that are large enough to hold a decent sized crowd, 24-hour access, and rooms with beds when consciousness is no longer an option. The downside is that they can cost a lot money, especially for the more upscale venues.
A few years back OrcaJam was held at a local hotel at a cost of $1,200 CDN for the weekend, which was mid-range for the city of Victoria, where the prices range from $400 to $2,500 CDN. In addition to room rental cost, some venues will require spending a mandatory minimum amount on catering, which brings us to our next subject: food.
Fuel for the Gray Cells
Providing food isn't a requirement of game jams, but it's a welcome option if it fits into your budget. At least providing snacks and beverages is a good idea. If you're permitted outside food at your venue, you may want to check your local grocery store for prepared food trays featuring cold cuts, cheeses, crackers, fresh fruit and the like: satisfying finger food that isn't too messy. You’ll want to try to minimize any clean-up that you need to do after the fact.
If you go the full meal route, remember that that will probably include two breakfasts, two lunches, and three dinners over the course of 48 hours, depending on how you schedule things. Keep in mind that your jammers may have special nutritional needs, so plan to have some gluten-free, vegetarian, and similar options available.
For the last OrcaJam, we bought pizza, sushi, sandwich and wrap platters, muffin and pastry platters, fresh fruit and vegetables, and lots of pop, juice, coffee, and tea. For 80 attendees, it cost around $2,700 CDN.
Warning: Hotels often have a “No Outside Food” policy, so if you want to provide anything for your jammers, you’ll need to make use of their internal catering services. Catered food can be pretty expensive, often equalling the cost of the room rental, so you’ll want to be sure to budget for it. I recommend trying to get a deal with the hotel when booking the space to receive catering at a discount. More often than not, businesses will give you a deal, especially if you can guarantee a good turnout; and besides, it never hurts to ask.
Funding Fundamentals
You may be asking yourself how you're going to pay for all of this. Selling tickets to the jam is one way to help cover your costs, but finding the right amount to charge can be tricky. You want to be sure that you charge enough to recover some of your costs, and also make it appealing to prospective jammers.
Charge too much and people won’t pay it, but charge too little and people will think that your jam isn’t worthwhile. For OrcaJam, we charged $20 per ticket and offered a free event T-shirt.
Tip: Offering early-bird bonuses like ticket discounts or prizes is a good way to get an idea early on of how many people will be attending. For past OrcaJam ticket sales, we've used Meetup.com and Eventbrite and had a good experience with each. I recommend doing a little research on these and other sites to make sure you find one that fits your needs.
A great way to fund your jam is to find sponsors. Sponsorship can come from any number of different sources. OrcaJam has received funds from schools, local game companies, and other non-game companies. Besides money, companies can also donate jam space, hardware, game development software, and door prizes for the jammers. In return, they usually only want to be listed as sponsors at the event and have their logos prominently displayed. The last OrcaJam had six local game companies as sponsors who each contributed $1,000 to the cause.
But how do you find sponsors in the first place? Sometimes, all that’s required is putting the word out to folks who are part of your local game development community. If you're involved with a local group and have been getting out to some game events and conferences and networking with people there, you should have a pretty good idea of who to talk to.
Some of the past OrcaJam sponsors just came from chance encounters with people who happened to be looking for sponsorship opportunities. It goes to show: never doubt the power of networking. Sometimes it can pay off in a big way.
Volunteers Are Your Friends
Running a game jam can be a lot of work, especially if you try to do everything yourself. Why not save your health and sanity and enlist some help? Often other people will be happy to help out in exchange for a free pass to the game jam and a little recognition. Some folks will be looking for some volunteer experience to jazz up their resumes.
OrcaJam typically has about a dozen volunteers who help out over the weekend. We have at least two people staffing the registration desk at the entrance most of the time and at least one person there for the night shift. Other folks help with setup and cleanup, laying out meals, and whatever else needs doing. Their contributions make the whole event run more smoothly and make it a lot more fun for everyone involved.
Special Features
Let's see, you've got a game jam location, some funding, a bit of food planned, and some volunteers to help out. You've got the makings of a good jam. But what can you add to make it a great jam?
Theme
You should decide on a one-time theme for your jam. It can be anything you like, from one-word themes such as "Preserves" to abstract ideas like "We don't see things as they are, we see them as we are." (These were actual game jam themes.) Themes help to foster creativity by providing restrictions that can inspire and challenge the teams to make fun projects that incorporate them.
Keynote Speaker
Bringing in someone at the beginning of the jam to talk about making games and get the jammers excited about it is a great way to start things off. Think of someone you know who makes games and is a decent speaker, and send them an invitation. We had Christer "McFunkypants" Kaitila give a keynote last time and he really got the party started.
Panels/Talks
It's useful to give your jammers an occasional break during the weekend to keep them fresh. I find a great way to do this is to have a couple of events scheduled, one type being a panel or talk featuring game creators. Popular subjects include game design tips, DIY game publishing, and whatever else strikes your fancy.
Note: You may wish to not schedule anything for Sunday, as usually the groups are too focused on getting the last features in before the big reveal.
These are just a few ideas that you can adopt for your own jam. Bear in mind that anything that you add will require more organizing on your part, so be sure not to overload yourself.
Grand Finale
One of the best parts of a game jam is when everyone finally puts their tools down and gets ready to show off the results of all of their hard work. Each group should be introduced and then allowed to do a brief show and tell of their game, perhaps getting volunteers from the audience to try playing it.
Be sure to book plenty of time for this, as you don't want people to feel rushed. For OrcaJam, we not only scheduled dinner right before this, but we also brought in a keg of beer for this time so that all jammers (of legal drinking age) could relax, have some food, enjoy a beer if they wished, and check out the great games. It's a great way to end a fun weekend.
In a Nutshell
So there you have it: all you need to get your own game jam under way. If you have more questions, there are plenty of resources on the internet, including pages for existing and well-known jams such as Ludum Dare and OrcaJam.
If you’re looking for a game jam to jump into and be a part of, the Global Game Jam is just around the corner in January. You might want to check out its website to find locations near you. Or, if none are close by, you may just want to host one yourself. Because now you can.
Up until recently, all the tutorials on Tuts+ (there are nearly twenty thousand!) had one thing in common: they were all published in English. And that’s strange when you think about it, because our community is nothing if not global.
We often get requests for translations via social channels, email, and in tutorial comments, and we also know that community members are already translating our tutorials so they can share them with their fellow countrymen and women. In response, we’ve launched the Tuts+ Translation Project, to meet the demand and share the hard work of our generous community members.
Now it’s possible for volunteers to translate whichever written tutorials they please. We take the translations, give them the once-over, then publish them so that they’re accessible via the original posts.
Round of Applause
The response has been phenomenal. After a few initial requests via social media, over 60 volunteers have been in touch, publishing almost 200 translations in more than a dozen languages between them!
It’s Yours; Use It
Every written tutorial on the Tuts+ network is open for translation. If you feel you can help others by converting one into your mother tongue, find the link in the sidebar of a post you’ve enjoyed and let us know!
This project does rely on the generous nature of our community members (thank you) though translators are given full credit for their work. In fact, many have said that translating Tuts+ content is the best way to learn, so there’s certainly something to be gained by taking part.
Table Toppers
We’re very grateful to all who have volunteered, but a special mention has to go to these three individuals who’ve gone above and beyond in helping out with the project:
We’re continually adding more languages and translations to the network. Here are some of our favourites (with their ISO codes handily noted next to them):
Asset licensing is serious business. In a multi-billion dollar industry where thousands of hands come together under a single banner to create a single product, there should be no doubt as to who owns the art, code, and sounds associated with a game. But what about smaller developers that have to rely on externally licensed game assets?
Being an independent developer, you cannot afford to take any legal risks with licensed content. The rising popularity of asset stores and open source libraries has created an even larger need for a better understanding of game asset licensing. In this article, we take a look at the basics of copyright licensing for independent game developers.
Disclaimer
I want
to preface this article by stating that I am not a lawyer, I did not go to law
school, and this is not legal advice. If you are seriously concerned about any
licensing issues that may affect your game, then you should contact a legal
professional. This article can only get you acquainted with the basics of
licensing and is not meant to serve as a comprehensive legal resource.
You should always seek independent financial advice and thoroughly read terms and conditions relating to any insurance, tax, legal, or financial issue, service, or product. This article is intended as a guide only.
Can You Use This Asset in Your Game?
There are many intricacies involved with copyright licensing, but you can ask yourself a few important questions at the start before diving into the complexities of individual license requirements:
1. Did You Retrieve This Asset From a Reputable Source?
A reputable source will provide you with the proper licensing and attribution info for your asset as well as general information and FAQs regarding asset licensing. Examples of reputable sources include OpenGameArt.org, the Unity Asset Store, and Envato Market. Examples of non-reputable sources include Google image search and random forum posts.
1a. No, I Retrieved This Asset From a Non-Reputable Source
If the asset you want to use was not retrieved from a reputable source, then you should check to see whether that same asset is available from one of the many reputable asset sources available. You should also try to contact the creator of the asset directly, if you cannot verify the legitimacy of the asset.
If you are unable to verify the license of this asset, then you cannot legally use it in your game.
1b. Yes, I Retrieved This Asset From a Reputable Source
Continue to the next question.
2. Was a License Provided With the Asset?
A reputable source will usually provide you with the proper licensing information for the asset. The type of license supplied will determine how you are allowed to use the asset.
2a. No, a License Was Not Provided With the Asset
Contact the creator of the asset and/or the host of the asset for licensing information. If your are unable to obtain licensing information, then you cannot legally use the asset in your game.
2b. Yes, a License Was Provided With the Asset
You can legally use this asset in your game as long as you follow the individual terms of the licensing agreement. Continue reading for information on the types of licenses your are most likely to encounter.
Copyright: The Basics
Copyright is a bundle of rights that govern intellectual property. As an example, a spaceship sprite drawn by an artist on your favorite asset store is the intellectual property of that artist. That sprite is legally protected by copyright law. Through a public licensing agreement, that artist can grant non-exclusive permissions that will allow you to use that sprite in your creation (a derivative work).
Non-exclusive public license agreements do not require a written contract, but the licenses are still protected under copyright law. By using the asset, you are thereby agreeing to the licensing agreement terms.
Creative Commons (CC) Licenses
By using
assets from asset stores or an online asset library, you will mostly be subject
to a particular form of public copyright license known as a Creative Commons (CC)
license. Creative Commons is the most common type of non-exclusive public
license and can never be revoked by the creator. CC licenses consist of a combination
of different conditions:
Attribution (BY)– You can
use this asset (or a version of it that you have modified) in your game if you give proper credit to the
creator/licensor. Proper credit in this case is defined by the
creator/licensor.
Share-Alike (SA)– You can
use this asset (or a version of it that you have modified) in your game, but your game must be released under a similar
license to the asset in question.
Non-Commercial (NC)– You can
use this asset (or a version of it that you have modified) in your game, but only in completely non-commercial situations.
No Derivative Works (ND)– You can
use this asset in your game, but only if the original work is not modified in
any way.
An
example of a CC license utilizing a combination of these conditions would be aCC BY-NC license, where the creator of the asset requires proper credit and the
derivative work may not be used to generate a profit.
If your
CC licensed asset requires attribution, you are required to give proper credit
to the creator/licensor somewhere in your game. The creator/licensor gets to
determine how the asset is credited, and you will be in violation of the
license agreement if you do not adhere to the requirements.
When obtaining
licensed content from an asset store or library, you will be provided with the
details of the license. These details should also include the attribution
requirements. If the attribution requirements are not provided with a CC-BY
licensed asset, it is up to you to contact the creator/licensor for
clarification. If you have exhausted all efforts to attain the original attribution
requirements for an asset, you can follow the official Creative Commons best
practices for attribution guidelines.
Software Licenses
Aside
from licensing art and music, you may also be interested in licensing actual
code/software for your game. For software licenses, you will be subject to any
number of software licenses, including these free licenses.
GNU General Public License 3.0 (GPL)
The GPL
is a free software license that grants users the right to modify and share the
original work with a few important requirements. If you release a game that
contains any contented licensed by the GPL, then you must also release the
source code for your game and provide users with a copy of the GNU GPL license
terms and copyright notice.
This is
a free software license used in Mono development libraries, jQuery, Ruby on
Rails, Lua, Node.js, and Expat, among others. Software and code licensed under the
MIT license can be reused if a copy of the MIT license terms and copyright
notice are included with the end product. The MIT license is GPL-compatible.
Version 2.0 of the Apache license is a GPL-compatible software license, much like the MIT license, that requires the inclusion of the copyright notice and a specific disclaimer.
The
WTFPL is a license that was created as an alternative to other software
licenses. Under this license, you are allowed to "Do what the F you want to" with the licensed content. This is the least restrictive license available; any
assets licensed under WTFPL can be used in any way and attribution is not
required.
To put it simply: violating copyright law can cripple your project and ruin your game development career. Even the smallest copyright violation can have serious consequences. Both accidental and intentional violations are subject to the same penalties, so ignorance of the law is no excuse inside the courtroom. The legal penalties for copyright infringement are numerous:
Up to 5 years imprisonment
Criminal fines of up to $250,000 per offense
Up to $150,000 fine for each work infringed
An injunction to prevent the sale and distribution of your game
Payment of all attorney fees and court costs, and for damages and lost profits
License Violations: The Ethics
By violating copyright law and licensing agreements, you are effectively stealing from other creators. You are benefiting from the hard work of others without providing proper credit, and this can have serious consequences for your reputation as a game developer. The speed and coverage of social networks can bring unwanted negative attention to you, your team, and your entire project.
Conclusion
At
first glance, the legal mumbo-jumbo surrounding copyright licensing can seem
overwhelming. Keep in mind that these public licenses were created as a way tohelp artists and developers, and the content creators are doing you a huge
favor by making this content readily available without complicated contracts.
Take the time to read and understand your licenses and don’t forget to contact
the content creators once your game is complete. In the same way that you want people
to play your game, the artists that create publicly licensed content like to
know when their creations are featured. Maintaining a safe and friendly
relationship between developers, artists, and the law will help to ensure that the
game development community can continue to grow and improve in the years to
come.
It’s back! The Tuts+ annual survey for 2014 is here and we want to know what you think.
In having your say you could also win from a fantastic prize pool worth a total of more than $20,000! Plus there’s a special offer from Shopify and New Relic for everyone that completes the survey.
This is your opportunity to tell us what you like, dislike and love about Tuts+. You can also help us shape the future of your online learning by letting us know the features that would assist you with online learning.
This Cyber Monday (December 1st) we're reducing the prices of all individual Tuts+ Courses to just $3 (normally $15)!
Discover your new skills in everything from web design and code to photography and design & illustration with our step by step video courses. With expert instructors delivering easy to digest practical lessons, our 430+ courses include 1—3 hours of video, organized into chapters and bite-size lessons. You can learn it all at once or view a little each day.
Make sure you don’t miss out and leave your email here to be notified when the sale starts.
This offer is strictly limited to Cyber Monday and will not be extended.
In this tutorial we will wrap Web Audio in a simple API that focuses on playing sounds within a 3D coordinate space, and can be used for immersive interactive applications including, but not limited to, 3D games.
This tutorial is the second in a two-part series. If you have not read the first tutorial in the series you should do that before reading this tutorial because it introduces you to the various Web Audio elements we will be using here.
Demonstration
Before we get started, here's a small demonstration that uses the simplified API that we will be covering in this tutorial. Sounds (represented by the white squares) are randomly positioned and played in a 3D coordinate space using the head-related transfer function (HRTF) that Web Audio provides for us.
The source files for the demonstration are attached to this tutorial.
Overview
Because the simplified API (AudioPlayer) has already been created for this tutorial and is available for download, what we are going to do here is take a broad look at the AudioPlayer API and the code that powers it.
Before continuing this tutorial, please read the previous tutorial in this series if you haven't done so already and are new to the world of Web Audio.
AudioPlayer
The AudioPlayer class contains our simplified API and is exposed on the window object alongside the standard Web Audio classes if, and only if, Web Audio is supported by the web browser. This means we should check for the existence of the class before we attempt to use it.
if (window.AudioPlayer !== undefined) {
audioPlayer = new AudioPlayer()
}
(We could have tried to create a new AudioPlayer object within a try...catch statement, but a simple conditional check works perfectly well.)
Behind the scenes, the audioPlayer creates a new AudioContext object and a new AudioGainNode object for us, and connects the GainNode object to the destination node exposed by the AudioContext object.
var m_context = new AudioContext()
var m_gain = m_context.createGain()
...
m_gain.connect(m_context.destination)
When sounds are created and played they will be connected to the m_gain node, this allows us to control the volume (amplitude) of all the sounds easily.
The audioPlayer also configures the audio listener, exposed by m_context, so it matches the common 3D coordinate system used with WebGL. The positive z axis points at the viewer (in other words, it points out of the 2D screen), the positive y axis points up, and the positive x axis points to the right.
The position of the listener is always zero; it sits at the centre of the audio coordinate system.
Loading Sounds
Before we can create or play any sounds, we need to load the sound files; luckily enough audioPlayer takes care of all the hard work for us. It exposes a load(...) function that we can use to load the sounds, and three event handlers that allow us to keep track of the load progress.
The set of sound formats that are supported is browser dependant. For example, Chrome and Firefox support OGG Vorbis but Internet Explorer doesn't. All three browsers support MP3, which is handy, but the problem with MP3 is the lack of seamless sound looping—the MP3 format is simply not designed for it. However, OGG Vorbis is, and can loop sounds perfectly.
When calling the load(...) function multiple times, audioPlayer will push the requests into a queue and load them sequentially. When all of the queued sounds have been loaded (and decoded) the onloadcomplete event handler will be called.
Behind the scenes, audioPlayer uses a single XMLHttpRequest object to load the sounds. The responseType of the request is set to "arraybuffer", and when the file has loaded the array buffer is sent to m_context for decoding.
// simplified example
m_loader = new XMLHttpRequest()
m_queue = []
function load() {
m_loader.open("GET", m_queue[0])
m_loader.responseType = "arraybuffer"
m_loader.onload = onLoad
m_loader.send()
}
function onLoad(event) {
var data = m_loader.response
var status = m_loader.status
m_loader.abort() // resets the loader
if (status < 400) {
m_context.decodeAudioData(data, onDecode)
}
}
If the loading and decoding of a file is successful, audioPlayer will either load the next file in the queue (if the queue is not empty) or let us know that all the files have been loaded.
Creating Sounds
Now that we have loaded some sound files we can create and play our sounds. We first need to tell audioPlayer to create the sounds, and this is done by using the create(...) function exposed by audioPlayer.
var sound1 = audioPlayer.create("sound-01.ogg")
var sound2 = audioPlayer.create("sound-02.ogg")
var sound3 = audioPlayer.create("sound-03.ogg")
We are free to create as many sounds as we need even if we have only loaded a single sound file.
var a = audioPlayer.create("beep.ogg")
var b = audioPlayer.create("beep.ogg")
var c = audioPlayer.create("beep.ogg")
The sound file path passed to the create(...) function simply tells audioPlayer which file the created sound should use. If the specified sound file has not been loaded when the create(...) function is called, a runtime error will be thrown.
Playing Sounds
When we have created one or more sounds, we are free to play those sounds whenever we need to. To play a sound, we use the aptly named play(...) function exposed by audioPlayer.
audioPlayer.play(sound1)
To determine whether to play a looped sound, we can also pass a Boolean to the play(...) function. If the Boolean is true, the sound will loop continuously until it is stopped.
audioPlayer.play(sound1, true)
To stop a sound, we can use the stop(...) function.
audioPlayer.stop(sound1)
The isPlaying(...) function lets us know whether a sound is currently playing.
if (audioPlayer.isPlaying(sound1)) { ... }
Behind the scenes, the audioPlayer has to do a surprising amount of work to get a sound to play, due to the modular nature of Web Audio. Whenever a sound needs to be played,audioPlayer has to create new AudioSourceBufferNode and PannerNode objects, configure and connect them, and then connect the sound to the m_gain node. Thankfully, Web Audio is highly optimized so the creation and configuration of new audio nodes rarely causes any noticeable overhead.
sound.source = m_context.createBufferSource()
sound.panner = m_context.createPanner()
sound.source.buffer = sound.buffer
sound.source.loop = loop
sound.source.onended = onSoundEnded
// This is a bit of a hack but we need to reference the sound
// object in the onSoundEnded event handler, and doing things
// this way is more optimal than binding the handler.
sound.source.sound = sound
sound.panner.panningModel = "HRTF"
sound.panner.distanceModel = "linear"
sound.panner.setPosition(sound.x, sound.y, sound.z)
sound.source.connect(sound.panner)
sound.panner.connect(m_gain)
sound.source.start()
Playing sounds is obviously useful, but the purpose of audioPlayer is to play sounds within a 3D coordinate system, so we should probably set the sound positions before playing them. audioPlayer exposes a few functions that allow us to do just that.
Positioning Sounds
The setX(...) and getX(...) functions exposed by audioPlayer can be used to set and get the position of a sound along the coordinate system's x axis.
The setY(...) and getY(...) functions can be used to set and get the position of a sound along the coordinate system's y axis.
The setZ(...) and getZ(...) functions can be used to set and get the position of a sound along the coordinate system's z axis.
Finally, the helpful setPosition(...) function can be used to set the position of a sound along the coordinate system's x, y, and z axes respectively.
The farther a sound is from the center of the coordinate system, the quieter the sound will be. At a distance of 10000 (the Web Audio default) a sound will be completely silent.
Volume
We can control the global (master) volume of the sounds by using the setVolume(...) and getVolume(...) functions exposed by audioPlayer.
The setVolume(...) function also has a second parameter that can be used to fade the volume over a period of time. For example, to fade the volume to zero over a two second period, we could do the following:
audioPlayer.setVolume(0.0, 2.0)
The tutorial demo takes advantage of this to fade-in the sounds smoothly.
Behind the scenes, the audioPlayer simply tells the m_gain node to linearly change the gain value whenever the volume needs to be changed.
var currentTime = m_context.currentTime
var currentVolume = m_gain.gain.value
m_gain.gain.cancelScheduledValues(0.0)
m_gain.gain.setValueAtTime(currentVolume, currentTime)
m_gain.gain.linearRampToValueAtTime(volume, currentTime + time)
audioPlayer enforces a minimum fade time of 0.01 seconds, to ensure that steep changes in volume don't cause any audible clicks or pops.
Conclusion
In this tutorial, we took a look at one way to wrap Web Audio in a simple API that focuses on playing sounds within a 3D coordinate space for use in (among other applications) 3D games.
Due to the modular nature of Web Audio, programs that use Web Audio can get complex pretty quickly, so I hope this tutorial has been of some use to you. When you understand how Web Audio works, and how powerful it is, I'm sure you will have a lot of fun with it.
Don't forget the AudioPlayer and demonstration source files are available on GitHub and ready for download. The source code is commented fairly well so it's worth taking the time to have a quick look at it.
If you have any feedback or questions, please feel free to post a comment below.
Planning a game project and staying on schedule is hard. To keep on top of things, I use a simple system—no apps required, just a pen and some paper. This system drastically improves my scheduling and development process, so if you're always falling behind, give it a try! I think it could help you too.
Why I Like the Dynamic Priority List
When I worked in the game industry, I experienced a “death march” first hand. A combination of two
large projects and poor scheduling lead to 18-hour days for months on
end.
Since then, I've studied many methods
for planning projects, ranging from wikis to specialized computer
programs, but I've found one tool that surpasses them all: a standard notebook.
Wikis and apps take time to learn and maintain, and most developers I know
would rather spend that time making a better game. As the old saying goes, "the best
tool is the one you'll use”; this is the amazing strength of an everyday notebook.
Starting Your List
The Dynamic
Priority List is simply a list of tasks that will evolve over time.
It begins with a broad overview of the tasks you need to accomplish to finish your game:
Obviously, this
isn't a full plan. Your goal here is simply to have a place to
visualize the big picture. It's too easy to forget about that options
menu or loading interface when you're neck deep in code and behind schedule.
Now, sit down and
work normally. The less time you waste with your new planning tool, the
more likely you are to use it.
Working
naturally, you'll get a clearer image of each element that needs to
be completed. Cross out the broad overview items such as “Gameplay” and
replace them with specific features that you need or are even just considering.
As each item
pops into your head, quickly add it to the list without taking your
mind off your current task. Don't worry about how to implement it or
whether it even fits the game, just add it to the list so that you can deal with it in the future.
Advanced Shorthand
Did you notice
the question mark and arrows on my list?
As you get used to using the list, you can leave notes for yourself. This shorthand can remind you of things at a glance, and you should customize this to fit your own development style.
For example, I use the following:
Question mark: For any task that I'll need to think about—either a design question, or how to implement
it.
Arrows: For any tasks that
are linked in logic or in code. These will likely be
implemented at the same time.
Slash: When I have two
options that I'll need to choose between, I'll link them with a slash.
Swear words: For anything that I
dread and don't want to do. This serves no valid purpose, but it makes me
feel good.
Remember,
customize your shorthand to what works for you!
Nightly Ritual
Every single
night, when I shut down my computer, I perform a series of tasks to
prepare for the next morning. I read the list and pick tasks that
should be implemented during the next work day.
I place one star
beside each of these “Priority” tasks. When I finish, I set the list next to my computer so I'll know exactly what to start on
in the morning.
If any starred
items aren't completed during the day, I add another star that night. This
increases the priority of each task with time, so that I can't put off
the hard or boring tasks for more than a day or two.
I end up with this:
One star (*): Things I'll consider working on.
Two stars (**): Things I
should be doing even if I'm not in the mood.
Three stars (***): Nothing else
will be worked on until this is finished.
Zero star
items are implemented when I have free time, motivation, and no major
priorities. This is usually reserved for dream goals that aren't vital
to completion, or for features that aren't feasible until later in development.
The AFK Ritual
From now on, take
this notebook with you whenever you step away from your computer.
While you're
eating lunch, read over the list.
Watching TV? Read over the list.
Going for a walk? Why not take the list and read over it?
Use this time to ponder how to implement tricky tasks, decide which choices best fit your design, and figure out which features need to be cut based on your current deadline. Update the list with potential solutions and changes.
While working, we all get tunnel vision. In the office we have managers who should be watching the big picture, but as indies we have to do that ourselves. Use your AFK time to be your own best manager.
Cleanup
If you find that your list is getting
messy after a few days, you're doing things right!
Every four to seven days you'll want
to take some AFK time to copy the remaining tasks to a brand new page. But
the value here is that it forces you to look at the project as a
whole.
Don't simply copy everything that
remains; think about each item individually, based on your current schedule.
If you're running low on time, reduce the priorities of some features, and cut others from the list.
It's your AFK time, so you're eating, watching movies, and
having fun right now. You have all the time in the world to make the
hard decisions. Use it!
You now have a clean list that is up-to-date with your current priorities.
By using these basic strategies, you'll get a much better overview of your project. This will improve your planning dramatically, without cutting into your development time. Learning to use that notebook will allow you to accomplish more with a fraction of the effort.
Conclusion
Even if you miss deadlines or fail a task, by keeping a list, you have a running log of your priorities. You can see what you accomplished each week and rethink the choices you made. This allows you to learn from each project regardless of its success or failure.
Every project is a chance to learn and improve the process. This simple list can help you to accomplish that.
In this article, we’ll begin with
the basics of the GameMaker Studio particle system and end with advanced
techniques and implementations. Think of this as a crash course to get you
familiar and comfortable with one of GameMaker Studio’s most powerful built-in
features. The demo and downloadable project file will allow you to
follow along with the article to see exactly what the particle system is all
about.
The GameMaker Studio particle
system is a cheap and easy way to create flashy effects for your game project. Through
a combination of particles and emitters, you can quickly create impressive
explosions, smoke, blood, shrapnel, and countless other effects. Similar effects
can be achieved by using individual objects, but the computing cost of the
built-in particle effects system is far cheaper.
To use the particle system in your
project, you'll need to understand GML (GameMaker Language), but once you
become familiar with the particle system, it is simply a matter of filling in
the blanks and experimentation.
Check out the demo below to see what we can achieve:
Particle System Basics
Particle Systems in GameMaker
Studio consist of three parts: the system itself, the particle, and the
emitter. You can create multiple
systems, and each system can contain multiple particles and emitters. Think of
the system as a container, with the particles and the emitters defined within.
Implementing a particle effect in
GameMaker Studio is a four step process.
First, you must define the particle
system itself.
Then, you define the actual particles that will be used within
that system.
Next, you have to define the emitter that will create your defined
particles.
Finally, you have to determine when and where the particle emitter
will appear in your game.
To teach you the basics of the
GameMaker Studio particle system, we’ll start out by creating this very simple
green particle effect.
Creating the Particle System
Creating the particle system is
as simple as defining a variable. We create an object called obj_first_particle
and place the following code in the object’s Create event:
FirstParticleSystem = part_system_create();
The particle system will adopt
the depth value of the object that the system is defined in, but you can also
set the depth separately with GML:
part_system_depth(FirstParticleSystem,0);
Remember that objects and
particle systems with a high depth value are drawn to the screen first. With a
depth of 0, our green particle system will appear above objects with a depth
greater than 0, and will appear below objects with a depth less than 0.
Particle systems are drawn to the
screen with a base position relative to (0,0). If for some reason you want to
create an offset for all future positions of this particle system, you can use
the following code to create a new base position (where ind is the particle
system):
part_system_position(ind, x, y);
With a new base position set to(10,10), a particle created at (25,25) will instead be drawn to (35,35).
Changing the base position of a particle system is rarely necessary, but you
may find it useful in your specific project.
Creating the Particle Type
Now that the system has been set
up, it is time to define the actual particle that the emitter will create.
Particles can contain a large number of parameters that dictate how the particle
will look and behave. The first step is to create a variable for the particle,
and we do this in the Create event of the obj_first_particle object:
first_particle = part_type_create();
Next, we begin defining the
individual parameters of the particle. Since we do not plan on altering this
particle during runtime, we can place all of this code in the Create event of
the obj_first_particle object.
part_type_shape determines the
base shape of the particle. There are 14 default particle shapes available in
GameMaker Studio, and you can define your own shapes as well. We’ll cover this
in the advanced section below, but for now let’s just start with a basic
square.
With part_type_scale, we can set
the base X and Y scale values of the particle shape. Since we want a perfect
square shape, we use the following code:
part_type_scale(first_particle,1,1);
part_type_size allows us to alter
the size of the particle at creation and as well as over time. The format for
this code is part_type_size(ind, size_min, size_max, size_incr, size_wiggle).
ind
is the particle variable.
size_min and size_max determine the range of the
particle size when it is first created. If you want a uniform size, simply
enter the same value for both the min and the max.
size_incr is a value that allows
the particle to grow or shrink over time. This value determines the speed of
growth, so if you don’t want your sprite to change size, you can use a value of
0.
size_wiggle is slightly more complicated, so we’ll cover that in the
advanced techniques section below.
Here’s the particle size code used in our green particle effect:
part_type_size(first_particle,0.10,0.15,-.001,0);
The particle will be created with
a size somewhere between 0.10 and 0.15 to create variety, and the sprite will
slowly shrink at a speed of -0.001. This speed value depends greatly on your
room speed, so you will likely need to experiment with values to get the
desired results. We will not be using any size wiggle, so we set the value to0.
Particles in GameMaker Studio can
actually change colors over time. This is achieved with part_type_color2 andpart_type_color3. If you don’t want your sprite to change colors, then you can
just use part_type_color1. For our green particle effect, we want it to start
out with a bright yellow/green color and then change to a solid green color, so
we use part_type_color2:
part_type_color2(first_particle,8454143,65280);
The two colors I selected are
specific numerical values that I use regularly, but if you want to use more
traditional hex values, you can use the format $RRGGBB.
Particles can also become more or
less transparent over time with part_type_alpha2 and part_type_alpha3. If you
want a consistent alpha value, use part_type_alpha1. For our green particle
effect, we want the particle to start completely opaque and fade by 25% as it stays on the screen, so we need two alpha values:
part_type_alpha2(first_particle,1,0.75);
In GameMaker, alpha is a value
from 0 to 1. A completely invisible object will have an alpha value of 0, while
a completely opaque object will have an alpha value of 1.
Particle speed is determined just
like particle size. Particles are created within a range of speed values, and that
value can increase or decrease. The format for this code is part_type_speed(ind,
speed_min, speed_max, speed_incr, speed_wiggle), where ind is the particle
variable, speed_min and speed_max is the speed range, speed_incr is the rate at
which the particle speed changes, and speed_wiggle is a parameter that we’ll
cover later on.
The speed code for our green particle is:
part_type_speed(first_particle,0.1,0.5,0,0);
Our particle will start moving
with a speed value somewhere between 0.1 and 0.5. This speed will remain
constant, so we use a value of 0, and we will again not be implementing speed wiggle,
so we use a value of 0.
While a particle’s speed parameter
determines how fast it moves, the direction parameter determines where it moves.
The direction code is in the following format: part_type_direction(ind,
dir_min, dir_max, dir_incr, dir_wiggle) and again we set the variable, starting
range, an incremental value, and a wiggle value. For the green particle effect,
we want our particle to start moving in any direction, and we want that
direction to remain constant:
part_type_direction(first_particle,0,359,0,0);
The range of 0 to 359 ensures
that the particle has a chance to move in any direction (an angle between 0 and 359 degrees). If you wanted a
particle to move up and only up, then you would use a range of 90 to 90).
The gravity of our particle
effect is what makes it the most interesting. While our speed and direction
parameters are set to create a particle that starts by moving in one direction at a
constant speed, the gravity parameter kicks in and alters the particle over
time. With a format of part_type_gravity(ind, grav_amount, grav_direction), the
gravity parameter is very simple:
part_type_gravity(first_particle,0.02,90);
By applying a slight
gravitational pull of 0.02 in an upward direction (90 degrees), we can create a
particle that appears to float. Combined with our size and alpha parameters,
the particle shrinks and becomes more transparent over time, accompanied by the gravitational
lift.
The orientation of the particle
shape is also important to the appearance of the effect, so we use part_type_orientation(ind, ang_min, ang_max, ang_incr, ang_wiggle, ang_relative) to rotate the
square over time.
ind is the particle variable.
ang_min and ang_max determine
the starting value of the shape’s rotation value.
ang_incr is used to increment
or decrement the shape’s orientation over time.
ang_relative is a Boolean value
to determine if the orientation should be set relative to the motion of the
particle (true) or not (false).
We want our green particle to rotate slightly to the left, so we use the following code:
One of the most important
parameters of a particle is the lifespan value. This value determines the
minimum and maximum time that a particle will be drawn to the screen. With two
identical min and max values, all particles of that type will exist for the
same amount of time. We want our green particles to have variety, so we will
use a range of 100 to 150 for the lifespan value:
part_type_life(first_particle,100,150);
The final parameter for particles
is a simple Boolean to determine whether the particles should blend together with an
additive blend effect:
part_type_blend(first_particle,true);
Creating the Particle Emitter
The first step in defining an
emitter is to create a variable. We define this emitter in the Create event of
the obj_first_particle object.
Next, we define the emitter
region with part_emitter_region(ps, ind, xmin, xmax, ymin, ymax, shape,
distribution).
ps is the particle system that the emitter belongs to and ind is
the emitter variable.
The x and y min and max values determine the size of the
emitter region.
shape determines the shape of the emitter region
(ps_shape_rectangle, ps_shape_ellipse, ps_shape_diamond, ps_shap_line).
distribution is a distribution curve (ps_dist_linear, ps_distr_gaussian,
ps_distr_invgaussian).
We’ll cover the shape and distribution parameters in
further detail in the advanced techniques section. For now, we’ll use the
default ellipse shape and Gaussian distribution values:
This code creates an elliptical emitter
region that is 40 pixels tall and 40 pixels wide, and centred on the x and y values
of the obj_first_particle object. Particles created by the emitter will appear
within this defined region.
Activating the Particle Emitter
The next step is to determine one
of two emitter types: Burst or Stream. A Burst emitter creates a specified
amount of a certain particle whenever it is triggered. A Stream emitter creates
a specified amount of a certain particle once every step.
We’ll take a look at
the more versatile Burst emitters in the advanced techniques section, so for
now let’s just use the Stream emitter:
We place this code in the Create
event of the obj_first_particle object, resulting in the emitter creating one particle each step as soon as the object is created. With a room speed of 30,
our emitter will create 30 particles per second; to create 60 particles per
second, you would simply use a value of 2 instead of 1.
And with that, our simple green
particle effect is complete! The usefulness of this effect is limited, but it’s
important to start small before diving into the more complicated aspects of the
GameMaker Studio particle system. Once you understand the basics of the
particle system, you can start implementing more advanced particle systems.
Advanced Particle System Techniques
Particle Wiggle
Wiggle is a simple, yet powerful parameter that can drastically change the
appearance of your particles. The wiggle parameter causes the particle to oscillate
between the min and max values for the lifetime of the particle. The value can
be between 0 and 20 and determines the speed of the wiggle.
The "Fire Bubble" example in the embedded demo uses a wiggle value of 0.40 in the part_type_size
parameter:
One of the most common implementations of particle systems involves particles
that emanate from behind a moving object, such as a smoke trail on a rocket. Achieving
this effect in GameMaker Studio requires a Burst emitter to be placed in an
object’s Step event.
The included example uses the
same green particle system as before, but with a slightly modified emitter. Instead
of triggering a Stream emitter in the object’s Create event, a Burst emitter is
placed in the object’s Step event. The current position of the mouse cursor is
checked against the previous position of the cursor, and if there is a change in the
cursor’s position, the Burst emitter is triggered to release five particles:
x = mouse_x;
y = mouse_y;
part_emitter_region(MouseParticle,green_mouse_emitter,x,x,y,y,0,0);
if x != old_x || old_y != y
{
part_emitter_burst(MouseParticle,green_mouse_emitter,green_mouse_particle,5);
}
old_x = x;
old_y = y;
Emitter Region Shapes
By using the different emitter
shapes and distribution curves, you can create vastly different effects. Linear
curve distribution combined with a line-shaped particle emitter can create a
convincing rain effect.
part_emitter_region(RainParticle, rain_emitter, -100, room_width, y, y, ps_shape_line, ps_distr_linear);
The emitter shape is defined by a
line that begins 100 pixels to the left of the room origin and is extended to
the width of the room. A linear distribution curve is used to distribute the
rain particles evenly across the emitter region. It is also useful to usepart_system_update to advance the rain particle several steps in the Create
event. This code gives the impression that the rain was falling before you
loaded the room, even though the particle system didn’t exist in memory yet.
Individual particles within a
system can also spawn other particles on Step and Death events. The example shows
a purple spark particle that spawns smaller dust particles as it travels to the
right, and spawns a smoke particle at the end of its lifespan:
The most important thing to
remember about the GameMaker Studio particle system is that you need to manually
remove items from memory when not in use. Unlike standard game objects, particle
systems will remain in memory even if you change rooms. The easiest way to
handle this is to place memory management code in the room changing events of
your game objects.
part_system_clear(ind): Clears all emitters and particles belonging to a specific particle system.
part_system_clear(ind): Clears all instances of a specific particle type.
part_emitter_clear(ps, ind): Clears all particles belonging to a specific emitter.
part_emitter_destroy(ps, ind): Destroys a specific emitter in a specific particle system.
part_emitter_destroy_all(ps): Destroys all emitters in a specific particle
system.
part_type_destroy(ind): Destroys a specific particle type.
part_system_destroy(ind): Destroys an entire particle system, including all
particles and emitters contained within.
Conclusion
Even with such a lengthy tutorial, we've still only scratched the surface of what kinds of effects the GameMaker Studio particle system is capable of. The key to mastering particles is familiarity and experimentation, so jump in and start creating your own effects using the knowledge you've gained. Be sure to check out the official GameMaker documentation to view all the available particle system GML!
Ever since the Ouya successfully kicked off the microconsole concept, several other microconsoles have launched too, including the GameStick, the PS Vita TV, and most recently the Amazon Fire. They exist beside the established home consoles, such as the PlayStation 4, Xbox One, and Wii U, and offer smaller games that are more comparable to mobile titles. Many of these are, in fact, ports of former mobile games.
These microconsoles offer developers a new way to get games out there. They are plugged into TVs, and offer similar processing-power to mobile devices. As most are Android based, such as the Ouya, the GameStick, and the Amazon Fire, creating and porting games for them is much simpler than having to re-create them for each system. Many engines (Unity, Flash, Game Maker) that already offer deployment to Android can be easily adapted to port to microconsoles, and also offer documentation on that process.
I have ported one mobile-game to the Ouya (Vertical Void DX), created one exclusively for it (Strike Craft Copperhead), and helped port one while working for a company, where I also handled the majority of a port to the GameStick. While doing that, we had to adapt the (former) mobile games to the environment of being played on a television instead of a phone or tablet, where we encountered some unforeseen issues.
The lessons below are ones I learned primarily from designing for the Ouya and GameStick, but are valid (and important!) when designing for other microconsoles, too.
Use a High Screen Resolution
The games are going to run on a television, so you should aim for performance on a high resolution: 1080p(1920x1080px) should be the goal. At the very least, a game should run and support 720p (1280x720px) as a basic resolution.
Look for Glitches on Larger Screens
Running a previously mobile-only game on a TV in 1080p means you suddenly have a completely different view of it. What previously might have been invisible could now be a very visible glitch. Edges that are not anti-aliased edges can become more prominent, and what looked good on a small phone may look worse on a TV.
Remember to check for these issues and play through the whole game looking out for them, so that they do not surprise you.
1080p Requires More Processing Power
Having a higher resolution also means having more pixels to crunch, so make sure you test performance. If your game is already straining on mobile devices, outputting more pixels could be an issue.
The Ouya is comparable to a decent Android tablet in terms of performance, so if your game runs on a Nexus 7 there's a good chance it will run well on the Ouya.
Adapt the UI for TV Displays
As there are a wide range of TVs, which include older models, it is not 100% possible to guarantee that the game's image will fit perfectly on every screen.
Older TVs, in particular, tend to overshoot the image at times, cutting off the sides of the screen by a sometimes considerable amount.
To ensure that everything is displayed correctly, every console game needs to have a "safe zone", which is a border on the edge of the screen where nothing critical is shown. On the Ouya, this border is 15% of the screen's width.
This is a screenshot of Strike Craft Copperhead, with the interface as it would appear on a desktop or mobile-game, where UI elements are close to the edge of the screen:
For TVs, however, we need to think of the aforementioned safe zone. Here is the same image with a 15% safe zone marked:
Nothing vital to the game can go into that part, so we must move all interface elements inside it:
In addition, you can add an option for players to adjust the interface position themselves, so that they can make sure it is in a good position, and so make best use of the available screen space.
Add Complete Gamepad Support
Every microconsole game needs to be able to be controlled with a gamepad, which means that some kinds of games (such as strategy games) could be difficult, or even impossible, to control well.
Unless you can come up with a workable control scheme, such genres will remain very difficult to port.
Avoid the Ouya Touchpad
The Ouya controller has a built-in touchpad, so you might assume that mouse-based controls would work well for such games.
However, in practice, the touchpad is really only useful as an emergency input when running mobile apps on the Ouya. It will not work as a mouse substitute, as the speed, sensitivity, and comfort levels barely even approach those of mice.
Make sure that everything works well with just a controller.
Support Xbox, PlayStation, and Other Gamepads
There are ways to recognize what kind of gamepad is connected. Unity, for example, has a function called Input.GetJoystickNames(), which outputs a string that identifies a controller.
An
Xbox 360 Controller would show up as "CONTROLLER (XBOX 360 FOR
WINDOWS)"; a PlayStation 3 controller as "PLAYSTATION(R)3 CONTROLLER".
This
allows us to differentiate different types of controller, and to tailor the game to work well with all gamepads we want to support.
Allow Hot-Plugging
Hot-plugging is the process of plugging in a device while the software runs. Ideally, we want our games to be able to support that; otherwise, the gamepads we use to control the game would all have to be already
plugged in when we load up the game first, and wireless pads would have
to be booted up. Then we'd have to make sure the wireless pads don't
enter sleep-mode, which would necessitate re-booting the game to prevent
such annoyances. This still often props up even in AAA releases.
So, let's not do that. Let's make sure that hot-plugging works.
If
you already a have a system to recognize what kind of joystick is
plugged in, you can adapt the code right there. What we will need to do
is check which gamepads are connected all the time.
This
does not have to happen every single frame, in case you're worried about the
processing power. Checking between every frame or every second
should suffice.
Design Menu Types to Suit Gamepads
There are different ways to set up menu systems for your games. If your game does not support directional menu controls (via a gamepad or keyboard), it needs to be adapted or rebuilt.
One way to do is to label each function with an image of the corresponding button, as seen here in Strike Craft Copperhead:
A better controller-friendly menu system would be one with highlighted buttons. This feels much better, but is also more difficult to create. Here we can see it in Vertical Void DX:
Regardless of the type of menu, it is always useful to have a "legend", explaining what each button does, like in the image above.
Also, remember to use the established way of using certain buttons. The "lower" action button (A on the Xbox, X on the PlayStation, O on the Ouya) is usually the forward button, while the button on the right is to go back. The central system button should be used to bring up menus or to quit the game.
Use "Side-Loading" to Test Ouya Games
The Ouya offers the ability to run any other Android app out there, without it having to be specifically adapted for the microconsole. This process is called side-loading. When any app is on the system (downloaded via the web, or copied via a USB stick), it can be installed and run.
If you already have an Android game programmed and ready, you can just put it on the Ouya and see how it would work!
Side-loading can also make it easier to deploy to the Ouya. If you have AirDroid running, for example, you can copy new builds via Wi-Fi, which makes the process far quicker and simpler.
Know the Difference Between Ouya and Android
Even though many microconsoles are Android-based, the platforms have to be approached separately.
As we have already discussed, every Android game can also be run on the Ouya. But to become an actual Ouya game, it needs to be adapted and to have all the necessary plugins configured. Some Ouya-specific settings can also override settings in an Android build, so you must consider those separately to prevent problems.
Price Your Games Appropriately
Microconsole games are smaller than AAA-titles, but can be more complex than mobile-titles. The pricing of microconsole games leans closer to the mobile side of things, with prices around $10 being acceptable. Towerfall, one of the most successful Ouya games, had a price of $15 for a long time.
Free-to-play games are also allowed, and microtransactions are supported, at least on the Ouya.
Implement Offline DLC Checks
Usually games check whether the player has bought DLC packs on startup, to make sure they are there before the game starts.
This system breaks, however, when no internet-connection is available.
An alternative would be to use an offline variable, like Unity's PlayerPrefs, to check whether packs were bought. This way, the game with all its content can be played offline too, and a (possibly long) check period at startup can be avoided.
Consider App Store Age Restrictions
Pretty much every major app store (including the iOS App Store and Google Play) restricts "adult" content, and apps for an 18+ years audience are not allowed.
Add a Quit Button
Traditionally, iOS games lack an in-game quit button. On Android, however, those appear quite often.
A quick survey of games on the Ouya store shows that around half have a quit button. It seems worthwhile to add one to our microconsole games as a matter of habit.
Conclusion
We have learned about the major (and some minor) design elements that we have to consider when making a microconsole game. While these aren't the only ones that will come up, this list should give you an idea of what to expect when targeting the microconsole platform.
Released early
this fall, Destiny was arguably the biggest game of the year. As a massive undertaking from Bungie, expectations were high upon
its release and sales followed accordingly.
Now that the game
has been out for a while, it's become apparent that some marketing
snafus and a few key design mistakes held Destiny back from becoming
the game everyone was hoping it would be. In this article, we'll take
a look at how and why Destiny failed to meet its lofty expectations,
as well as what these shortcomings can teach us about game design.
1. Don't Over-Promise and Under-Deliver
Few games have
ever had as much hype as Destiny. Excitement for the game
started building even before it was announced; as soon as Bungie
declared that they were moving away from the Halo franchise, gamers
everywhere were giddy with anticipation for their newest project. The
Halo games were all top-of-the-line products, with tight multiplayer, and cinematic campaigns that resonated with many people. Destiny was
expected to be great even before anybody knew anything about it.
Then the marketing
machine rolled in and the hype got out of control. The game's initial
reveal was nothing more than some concept art and some story details
from a leaked document, and people were already hooked. Eventually the game
was officially revealed to the media as the first instalment in a
series of games to be released over ten years. Everything about
Destiny just screamed epic, and media outlets and trade shows had a
never-ending supply of new details, previews, and trailers for the
game.
Unfortunately,
this massive hype contributes to Destiny's most obvious flaw: it
over-promised and under-delivered. All of this press was filled with
details about the game, and how it was to be one of the biggest and
most expansive experiences the medium had ever seen. Players were
told they could explore their entire solar system in a massive
multiplayer open world, with a strong narrative to tie it all
together and a solid competitive shooter included on the side.
The game that
actually launched is a pale imitation of the game that was promised, containing
only a few planets in our solar system, and a narrative that manages to be cliché and
boring and yet also border on incoherence. The gameplay feel of the
shooting and movement is excellent, but the game has nowhere near the
amount of content people expected, and much of what was promised
simply never came to fruition. Even worse, trailers for the game even
just months before release featured characters, cutscenes, and
environments that are nowhere to be seen in the final product. This
type of marketing is disingenuous, and does nothing but
leave a sour taste in players' mouths.
It's unclear at
this point whether this was an intentional deception on the part of
Destiny's marketing team, or if there were more complicated issues
that led to the discrepancies between what was advertised and what
was released. Game development can be complicated, and things don't
always go as planned. What we can learn from this is clear however: don't promise more than what you are confident you are capable of
delivering.
Destiny is not a
terrible game by any means, and actually has a respectable amount of
content for a triple-A shooter, but because of the way it was
marketed, players were left feeling let down when they otherwise
might not have been. Promising more than you realistically expect to
deliver does nothing but create disappointment and animosity for the
final product, regardless of its quality or the amount of content it
actually offers.
2. Progress Gated by Random Outcomes is
a Bad Idea
It seems like
every game needs to have RPG elements these days; loot systems, skill
trees, and character classes are popping up more and more in genres
where you typically wouldn't expect them. Like Borderlands before it,
Destiny brings the RPG to the shooter, with questing and character
progression being just as important to its core design as landing
headshots and well-timed melee attacks. However, Destiny's levelling
system is unconventional to say the least, and the game suffers
tremendously for it.
Just like almost
every other RPG, Destiny initially allows players to accrue
experience from killing enemies and completing objectives. When
players level up they learn new skills and get access to new gear,
just as you would expect. The system is common because it works,
and working towards the max level of 20 is great fun. Unfortunately,
players quickly learn that 20 isn't really the level cap, it's the highest level that can be gained from traditional experience
points. Players can continue to level up, but only by finding and
equipping pieces of high-level gear. The better your gear, the higher
your level.
Many MMOs thrive
off this type of progression system, with players being required to
constantly find more powerful loot to tackle more difficult
challenges. The cycle of gaining power to defeat enemies you couldn't
before is fun and engaging in many games, but Destiny just doesn't
do it right. Higher-level gear isn't really better than lower-level
gear as much as it's just higher-level. The only perceivable
difference between lower-level gear and higher-level gear is that
your character's listed level goes up.
Since it's
virtually impossible to kill enemies that are a higher level than
you, your ability to access end-game content is arbitrarily
gated by the level of the gear that you have found. This poses a
problem because gear drops are strictly random, there is a small
chance at any point of finding good gear, and so you simply need to
repeat enough of the lower level activities and hope you get lucky
with your drops. It is possible to buy decent gear, but the amount of
currency required for even just one piece is absurd. The grind is
immense and unreasonable, even when compared to other games in the
genre.
All of this means
that progression in Destiny's endgame is entirely tied to outcomes
outside of the player's control. Since there are no reliable ways to
increase the chance of getting good loot, players no longer have any
agency in their own progression as soon as they hit level 20, leaving
the game feeling like a glorified slot machine. Having progression
tied to blind luck is incredibly frustrating for players, and is
simply an example of bad game design. The lesson here is obvious: always make sure that your player's actions have a role in
dictating the outcomes of your gameplay systems.
3. Temporary Content Needs to Have a
Reason to be Temporary
Timed content is a
big part of the design plan for games that hope to harbour active
online communities. Having temporary unique activities for players to
participate in is fun for active players, and encourages others who
may have lost interest to return to your game. Destiny absolutely
loves timed content: there is a constant rotation of weekly and daily
missions, as well as timed shopkeepers and weekend-only multiplayer
modes. Unfortunately, Destiny once again struggles to get this
tried-and-true concept right.
The issue lies
primarily in the game's inability to convince players that
there is a reason for its content to be time-restricted. Timed
content in most games is typically restricted to thematic events,
like lore-based or seasonal activities, or to content that differs
mechanically or stylistically from the game's core experience. They
act as fun diversions to regular content streams, and this is usually
well communicated to players.
Destiny's timed
content fails because much of it doesn't feel like it should be
temporary. A shopkeeper integral to character progression shows up
only on weekends, and three of the seven PvP modes advertised before
release are only available at select times. Rather than excite
players with bonus content, these timed offerings only bring about annoyance when they aren't there. It becomes irritating when you
can't play your favorite PvP mode on a Thursday, or you can't buy the
new helmet you've been saving up for until the weekend rolls around.
It's also worth
noting that announcing modes before release and then having them be
limited to timed content is really not a good idea. It leaves players
feeling disappointed when they first boot up the game and find that
they can't participate in something they were expecting to have
access to. Even worse, it gives them the impression, accurate or not,
that the content has already been created and is being withheld from
them in order to extend their interest in the product. Players'
impressions are important, and it's never advisable to present things
in a way that leaves them feeling deceived.
There isn't an
innate problem with games having temporary content, but the content
needs to be of an appropriate nature so that players can understandwhy access to it is limited. Not all of Destiny's timed content has
failed in this way—there was an interesting lore-based event not
long after release—but the majority of what they have done has
missed the mark. The key lesson to learn from this is that temporary
content is a game design tool just like any other; its value needs to
be well articulated to the player in order for it to have a positive
effect on their experience.
4. Don't Imitate Systems Without
Including the Core Functionality Players Expect
Destiny plays like
the video game equivalent of a top 40 compilation CD. It borrows
relentlessly from other games for almost every aspect of its design.
It feels like the kind of product people have daydreams about, where
they imagine the best parts of their favorite games all wrapped into
one package. There's nothing wrong with this; as I've said before, game design is an iterative process, and imitating others is a great
way to strengthen your own creation.
As much an MMO as
it is an open world shooter and a competitive shooter, Destiny has a
lot of different facets that make up its core experience. Each of
these three gameplay styles is heavily inspired by games that came
before it, and the core gameplay loops typical of these genres are
well executed and emulated in Destiny. Unfortunately, the game fails
to implement most of the conveniences that are common for games in
these genres, and this hurts the quality of the final product.
Destiny is an MMO
with almost no player interaction: voice chat is disabled by default
in most scenarios, and it's very difficult to partner up and
communicate with other players. It's also an open world shooter with
an almost completely static world: enemies always spawn at the same
places in the same numbers, and getting from any one point to another
plays out exactly the same way every single time. Finally, it's a
competitive shooter without voice chat, skill-based matchmaking or
any kind of map voting or veto system.
Without these
mechanics in place, each individual element of the experience feels
lacking and somehow incomplete. There's no specific rule that a
multiplayer shooter needs map voting, but it's something players have
come to expect from the genre, and its absence is obvious. The lesson here is that it's important to be careful when imitating others. We must take the time to understand what parts of other games
players simply like, and what parts they deem mandatory to their
enjoyment of the experience. Missing out on key conveniences and
mechanics is often distracting to players, and can even sometimes
impair their ability to enjoy a title at all.
Conclusion
Destiny isn't a
game that fails in every respect. It has superb shooting mechanics,
with great feeling moment-to-moment action. It also looks beautiful
and consistently runs smoothly, and the action is complemented by a
beautiful score. The game oozes craftsmanship out of nearly every
pore, but it fails to impress due to its crippling design weaknesses.
In the end, it
goes to show that even a perfectly polished game can fail if its core
design philosophies are misplaced.
One day, you come up with a great game idea that you think could be a hit. You and your spend countless hours bringing your idea to life, and then a few more to squash all the bugs.
Once you've finished, you decide to add a few video ads. You aren't quite sure where to add them, so you place them haphazardly without thinking about the best user experience. When you publish your app, the reviews come piling in: "There are too many ads!".
If this has happened to you, then you probably planned your monetization around your game instead of your game around monetization. Poorly implemented ads can decimate the launch of the next big app and this article will help guide you in the types of videos ads available, how to use the video ads, providers, and best practices for implementation.
Types of Video Advertisements
Video ads come in at varying length, but the industry standard seems to be 15 seconds, occasionally 30 seconds. Essentially, there are two types of videos advertisement (forced and un-forced) with two types of implementation (voluntary and involuntary).
Forced vs Un-Forced
Forced video advertisements do not allow the player to skip the video content. Some video providers calculate your revenue by percentage of the video watched, and forced video ads will always result in higher revenue. However, forced video ads can result in a poor user experience because they can interrupt the natural flow of the game.
Un-forced video ads allow the player to skip the video content by pressing a Close button. This type of video ads results in lower revenues, but can allow a better user experience because the user can skip the video content.
Voluntary vs Involuntary
Voluntary ads are considered opt-in ads where the user has to initiate the video advertisement. With these, the user is usually rewarded with a virtual item for watching the ad.
Involuntary ads are videos that appear without the user's consent. These are commonly displayed between levels or at certain milestones within the game.
How to Implement Video Ads: A Quick Overview
Although each platform will offer a different API, you should have access to some basic functionality with implementing video advertisements. Most platforms should at least have the following:
isAdAvailable()
playAd()
Note: This tutorial focuses on pseudocode and the general concept of displaying video ads. Please refer to your platform's documentation for detailed information on integrating video ads.
First up is the function isAdAvailable(). Most platforms should support this and it will return true or false depending on whether or not a video ad is available. Some platforms go so far as to download and cache the next video for you!
Next, all platforms should support the playAd() function. When you call this function, you’ll display the full screen ad within your app.
When you want to display an app, you’ll want to check to see if an ad is available and if a video is available display it. This can be done by first calling isAdAvailable() and then calling playAd().
Video Advertising Providers
With the popularity of video advertisements growing, there are a lot of video content providers available. If you are just starting out, you may want to see if your platform supports Vungle or AdColony. Both platforms have widespread adoption, and have been implemented very well.
Games Built for Video Ads
In today's market,
there are new game genres popping up all the time—clickers (like Bitcoin Billionaire) and animal simulations (like Crazy Goat) being two recent examples. With a
better understanding of video advertising, let’s dig into what types of games
are a good fit for video ad monetization.
Is Your Game Paid or Free?
If your game is paid,
your monetization strategy is solved in one step. The consumer pays for your
app once and most games do not ask for additional purchases.
If your game is free,
it’s an unwritten truth that, although most consumers dislike advertisements,
they will be willing (and will most likely expect) to see ads in it.
What Type of Game Are You Creating?
Certain genres lend
themselves better to video advertisements than other genres. Casual
games and action games can be a perfect fit due to the way player
progression is set up, while other games, such as RPGs or puzzle games, often are not.
In general, if your game uses a built-in monetary system using virtual items for
player progression or if you reward the player with virtual items, your app is
probably a good fit for video advertisements.
Where Can You Integrate Video Ads?
Instead of injecting
video ads in-between levels and forcing players to view the ad, your app will
have a better player experience if they opt-in to the videos. Although there
are plenty of creative ways to integrate ads, your integration points can focus
on pre-gameplay, during gameplay, and post-gameplay. However, the structure of your game might not provide natural break points where ads could be inserted.
Pre-gameplay: If your game is split into levels or stages, or has typically short "bursts" of gameplay like Flappy Bird, you could offer the player a power-up or boost that will affect their performance in the next level or on their next attempt.
During gameplay: If there are points in your game where the player will naturally pause to think about the answer to a puzzle or how to progress, you could offer the player a hint.
Post-gameplay: If your game features levels or stages, or has short bursts of gameplay, or if it is possible for the player to fail, lose a life, or die, you can offer the player the chance to increase the score or virtual items they obtained on their last run, or to pick up where they left off.
If you choose to
offer a reward system based on video ads, you need to consider what type of
bonuses you’ll offer to balance the game play. Otherwise, you could end up with a game
that’s very lopsided!
Examples of Integrated Video Ads
Video advertisements
that are integrated well can add to the overall experience of your game, and a
happy user tends to leave happy reviews. Instead of just placing video ads at
the end of a level or in the middle of the gameplay experience, let’s look at some
real case examples of ways that you can integrate video ads in your next app.
Player Progression
One popular way to integrate video advertisements is to allow player progression after watching a video advertisement. Instead of forcing the player to purchase an in-app item, you can give them the option to watch a short ad and reward them with an in-game hint or boost. In the screenshot below, the player can tap the star icon to watch a 15 second video to receive two hints that are instantly applied to the puzzle.
By allowing them to opt-in to a video, the app isn’t forcing advertisements on the player, the player is happy, and the app still generates revenue.
Word games aren’t the only way to allow player progression with video ads. If the app is a fighting game, the app could allow the player to watch a video for faster punches or stronger bullets.
Rewards
Another way to implement video advertisements is to allow the player to increase their rewards after watching a short ad. This feel-good experience leads to a better user experience, and can lead to recurring revenue for the app. If this option is provided any time the user earns a reward, the potential revenue could be more than an individual in-app purchase.
One example of using video ads for rewards is in doubling the player's experience points. If the player beats a level and earns 25XP, you can give the player a chance to earn 50XP instead, if they watch a short ad.
Another example is the way that the popular game Bitcoin Billionaire incorporated video ads. When a drone appears, the player can tap the box to receive a random positive or negative event. If the event is positive, they can potentially double the reward by watching a short video.
Note: I spent more time in Bitcoin Billionaire than I care to admit, and I watched a lot of ads to double my rewards.
Restart From Checkpoint
Another way your app can offer an incentive for watching an ad is by allowing the player to pick up where they lost. This is especially useful in games that are level-driven with set beginning and end points. For example, if the player crashes half way through the race in a driving game, you could offer the player a chance to watch a short video to pick up where they crashed.
Closing Notes
After reading this article, I hope that you have learned how to get started monetizing your app using video advertisements, and how to use them effectively.
I've focused heavily on games, but video advertisements can be used within non-game apps as well. However, you are more limited with the usage of video ads; they should be placed at natural breaks in the content.
Thank you for taking the time to read this article. Are there other ways that you are currently using monetization? I'd love to hear them!
Crafting has expanded from an rarely-seen mechanic in role-playing games to a nearly ubiquitous inclusion in all modern titles. It's now not only used in nearly every RPG, but also in first-person-shooters, action games, driving simulators, and even Steam's user profile badge system.
There's something thematically appealing about transforming base resources into useful goods, and players enjoy the strategy and choice that customization can enable. Though widely used, there is substantial variation in the appearance and implementation of crafting systems. In this article, I categorize these systems into five approaches, and highlight what works best about each and how they are best used.
Type 1: "Money by Another Name"
As Seen In: Dragon Age: Origins, The Elder Scrolls V: Skyrim (Blacksmithing), The Legend of Zelda: Skyward Sword, XCOM: Enemy Unknown, Assassin's Creed III, Terraria, Dungeons of Dredmor, Kingdom of Loathing, Bioshock, and many more.
This is the most common way games feature crafting, but it's actually only "crafting" in a conceptual sense. Here, the player pays for a desired item or upgrade, with resources that they've collected, in a fixed and transparent exchange. The so-called crafting here is abstract: you hand over some units of wood to a craftsman or workbench, and they hand you back a wooden shield.
This is generally indistinguishable from an in-game market system, but instead of using a money themed resource like gold coins, we're using a raw material themed resource, like iron bars, oak logs, or wolf pelts.
Advantages
Due to its simplicity, this type of crafting is immediately familiar and easy to grasp. The inputs and outputs of the system are clear and unambiguous, so the system requires little or no explanation. Because it leverages existing systems, it also makes it easy to implement, and sometimes even employs the exact same interface that shopping with "money" uses. It's an effective way to add flavor and color to a market system, as the metaphor of turning raw goods into products can be richer than that of purchasing products with money.
This system can also provide a balancing function to the game by allowing additional control over what the player has access to. By making the currency exchanged for a given item rarer, or available only at a certain point in the game, the designer can disguise the true cost of the object. For example, if a cool sword costs 1,000 gold, the player might grind to save that up or think that costs too much and ignore it. But if it costs 50 steel, then it may seem easier to attain, even if it takes the same amount of time and effort to gather one unit of steel as it does to gather 20 units of gold.
Disadvantages
This system's simplicity is its own drawback, and it's typically insufficient for games striving for more immersion. It's also difficult to strike the right balance between being a small but flavorful inclusion and feeling extraneous and tacked on. Plus, if too many resource currencies are used, it's very easy to make the system overcomplicated and tedious.
In Assassin's Creed III, for example, the crafting system uses dozens of ingredients. Crafting one unit of Firearms, which has no use besides being sold for money, requires combining Weapon Handles (themselves crafted from Maple Lumber), Iron Ore, and Flints (itself made from Limestone, Lead Ore and more Iron Ore). So much detail can easily exhaust the player if the rewards for navigating such a dense system are too small.
Type 2: "Find the Recipe"
As seen in: Dragon Age: Inquisition, Diablo III, World of Warcraft, Dead Island, Fall Out 3/New Vegas, The Witcher 2, Arcanum: Of Steamworks and Magick Obscura
In this system, crafting a given item can't be done until a representational recipe, schematic, or blueprint has been found that unlocks it. These recipes may be found in the world, given as rewards for completing challenges, or purchased from stores.
Once the player acquires the recipe, crafting proceeds just as with "Money by Another Name" by exchanging resources for outputs in a fixed fashion; the difference here is that the player needs not only the underlying resources for the exchange, but also the knowledge that the exchange can be made at all. More rarely, the recipe might unlock a more customizable form of crafting, as it does in Dragon Age: Inquisition.
Advantages
This system can provide some additional flavor to the crafting process, as it represents the idea that merely having resources isn't enough; the player must also know how to combine them. Also, by adding a step to the crafting process, this effectively increases the number of rewards a player can be given without increasing the total number of items. This has some completionist appeal too, as the blueprints must be hunted down and collected.
It also maintains surprise for the player, since the full extent of what can be acquired isn't known right away, unlike in a shop-like system. Lastly, it provides the designer with an additional gating mechanic by preventing players from accessing certain content too early, even if the requisite resources themselves are available.
Disadvantages
You must take care in balancing the difficulty of acquiring schematics and the underlying resources needed with the value of the items they produce. If the reward is insufficient, then the system will largely be ignored in favor of items acquired through other means (like stores or enemy loot). Conversely, if it's too good, it can displace the other avenues of item acquisition.
As with "Money by Another Name", it can be prone to over-use due to the simplicity of implementation, and acquiring all the schematics can thus become a chore. Dragon Age: Inquisition, for example, has over 200 weapon and armor schematics, and they must each be found or purchased individually.
Type 3: "Guess and See What Sticks"
As seen in: Minecraft, Kingdoms of Amalur: Reckoning (Alchemy), The Secret World, The Elder Scrolls V: Skyrim (Alchemy), The Elder Scrolls Online (Alchemy), and Diablo II (Horadric Cube).
In this method, the recipes for crafting are neither immediately known nor acquired in-game, but must instead be discovered by the player through combinatorial trial and error. The player is generally presented with a crafting interface in which they can combine the resources they have collected, and is tasked with finding valid outputs by trying different arrangements of inputs.
The complexity of this can range from merely choosing different ingredients (as in Skyrim), to additionally choosing their quantity or physical placement (as in Minecraft). This makes the crafting combination not an abstract unlocking mechanism, but instead a literal recipe that the player must learn. Because of the ingredient-focused nature of this system, it's most commonly seen when the crafting is flavored as alchemy or cooking.
Advantages
Unlike "Money by Another Name" and "Find the Recipe", in which the crafting outputs are known or accessible, here there can be a sense of real discovery for the player. Rather than merely being told what the crafting system can create, the player is able to research and experiment in order to discover possible items they can create.
Though the crafting here is still abstract on some level, the process of discovering what works more closely approximates actual experimentation. It also makes the process of crafting require more direct player involvement, which can highlight the mechanic or its emphasis in the overall game design. Because what the player can test is limited to the resources they have gathered, it also provides the same balancing/restriction function as the previous types.
Disadvantages
If the system is too punishing for incorrect guesses (consuming rare resources, for example), then it may discourage players from actually exploring the system. If the correct ingredient combinations seem too arbitrary, then it may be feel less like a game of discovery and more like a random guessing game. In either case, if it is too punishing or difficult, the player will invariably look the answers up instead. In such cases, the system is really just "Money by Another Name", but with an additional layer of player frustration.
Also, such a system can become unduly laborious (and necessitate outside resources) if there are too many recipes to remember, or if the correct answers aren't recorded in-game.
Type 4: "Made-to-Order Customization"
As seen in: Assassin's Creed: Revelations, Dragon Age: Inquisition, Dead Space 3, The Elder Scrolls V: Skyrim (Enchanting), The Elder Scrolls IV: Oblivion (Spell Creation), Mass Effect 3, Monster Hunter Tri, Kingdoms of Amalur: Reckoning (Sagecraft), and Mercenary Kings.
This is any system in which the crafting is modular or dynamic instead of following a set formula or fixed exchange. Rather than a shop-like mechanic, here the crafting gives items a build-your-own flavor, where the player is able to pick from a set of choices.
In Assassin's Creed: Revelations the player can make bombs, and by choosing the casing, fuse and contents, they can make a variety of different explosives with different uses and effects. Unlike in "Guess and See What Sticks", where the player's "choice" affects a binary right or wrong outcome, here the player is actually having to commit to a strategic choice from a range of possible, known outcomes. This can be reflected in-game as choosing different materials for an item, different design elements or add-ons, or even purely aesthetic flourishes.
Advantages
This system permits an in-depth system of customization that can be very rewarding for players, and can give otherwise static items or equipment new versatility. Since the player has an actual impact on the outcome of the crafting process, it gives them more agency than they have in simply purchasing or finding a better item. And if the options available have distinct uses, trade-offs and drawbacks, then it can create an additional layer of strategic choice in the gameplay.
The increased flexibility and depth here can make the crafting process an integral part of the core mechanics of a game, instead of just as a flavorful method for producing items.
Disadvantages
This type of system has an increased level of complexity compared to the previous types, and while that provides additional depth, it also means that it is harder to implement. Increased complexity also requires more attention from the player, which might distract their focus if crafting is not meant to be a core game element. On the other hand, if the system has too few real choices, then its depth is largely superficial and will be optimized to a few "correct" choices, removing the advantage of using such a system.
Overall, due to the number of permissible outcomes, it can be particularly difficult to balance custom created items against items acquired from other sources.
Type 5: "Anything is Possible"
As seen in: Vagrant Story, The Legend of Mana, Treasure of the Rudra (Kotodama), Breath of Fire III (Dragon Genes), the Atelier Series, and The Elder Scrolls III: Morrowind (Spellcrafting).
This a broad category covering anything featuring a true crafting system, where the input variables can produce a wide array of possible outcomes. This allows for a truly dynamic process with very deep strategic choice, like the blade-and-grip combination system of Vagrant Story which has thousands of permutations. This can elevate the crafting system to being the primary game mechanic, as it is in the Atelier series of JRPGs. "Anything is Possible" systems are almost always the only source of the items they produce, because it can be very difficult to balance the outcome against something more easily available.
The complexity such systems introduce can take players a great deal of time to fully explore. The tempering system in The Legend of Mana is so complicated (and mostly undocumented in the game) that players are still discovering useful recipes for it nearly 15 years after the game's release.
Advantages
Investigating a system of this depth can be extremely rewarding. Having so much dynamic possibility can give the player meaningful agency in the process. The depth of these systems can greatly enhance gameplay or extend re-playability, and the difficulty of charting all possible outcomes can help the game resist optimization and the resultant stagnant gameplay.
This can be the least abstract form of crafting, since the immense variability of the system accords to the possibilities of actually making real things, and the testing and development the player must engage in is not unlike performing actual scientific inquiry.
Disadvantages
This level of depth makes these systems extremely hard to develop and implement, as they require substantially more design and testing than simpler systems. The resulting complexity is unnecessary for most games, where crafting is a supplemental mechanic rather than a major feature.
Because these systems sometimes have infinite range of outcomes, the possibility of game-breaking combinations of ingredients (as in, combinations that produce items that are too good) can be hard to plan for.
Conclusion
Crafting can serve a variety of purposes in a game's design. It can provide a thematic and flavorful way for the player to access items, whether it's smithing armor in a fantasy setting or applying futuristic gun mods in a sci-fi game. It can play a vital balancing role in how the player acquires upgrades, and with greater subtlety than more overt restrictions. And, if built with sufficient depth, crafting can even provide strategic gameplay in and of itself.
Crafting doesn't belong in every game, however. If tacked on without care, it can be extraneous and distracting, and if made complex for no reason, it can be tedious and burdensome. Crafting systems are a structural mechanic, and so they function best when used to support a game's primary design goals.
An overwhelming response to our 2014 survey created a small backlog in selecting the lucky prize winners. We've spent the past few weeks slowly making our way through each survey response, and we're excited to finally announce the winners!
Firstly we'd like to thank you for taking the time to tell us what you think, both the good and the bad. Your feedback will help us shape the future of Tuts+ and your online learning journey.
We've been inspired by your stories and we're overwhelmed with your desire to create better lives via self-directed online learning.
Over the coming months we'll be featuring interviews with some of the prize winners to give you the opportunity to meet some of your Tuts+ community. We will also be sharing some of the results of the survey. Please note, full names have not been shared below in order to protect the privacy of the winners. Each prize winner will also be contacted directly.
The Prize Winners
Grand Prize Winner: Catherine, South Africa
Lifetime membership to MadeFreshly Pro Plan (RRP $420/year)
Runscope team account for up to 25 team members and 500,000 requests per month (RRP $2,148)
Interactivity
is a fundamental element of games design. Without it,
a game isn't a game: it's a TV show, or a book, or an instance of some other static
medium. Interactivity is what really defines games, and is
(arguably) the single most important aspect for keeping players interested.
Sadly,
interactivity is also an element that often falls to the wayside.
We might get an idea for a cool game element, and become focused on
getting that to work without looking at the implications. Or, worse,
we ignore interactivity because "that's the way these games are
done".
There
are other ways to keep players entertained: the
incredibly divisive Dear Esther had very poor interactivity, but managed to find a fan base
nevertheless. So, if you're planning on making an art game, then the lessons here may not apply directly to you, but these aspects are important to be aware of
anyway.
The fact is, most players want to play—so locking them out is an incredibly effective way to frustrate them and make them lose interest. There are some obvious ways of doing this, such as including unskippable cutscenes or lengthy loading times, but we'll focus on game mechanics that lock players out, and how to avoid them.
Lockout
as Punishment
In
Monopoly, when a player loses all their money, they are removed from
the game. In Counter-Strike, when a player dies, they are removed
from the game until the next round. In both cases, the player
has “lost”, and, as a punishment, they are no longer allowed to
play.
In cases like these, you will find these players very quickly
losing interest in the game. In Monopoly, the losing player may have
to sit around for an hour or more waiting for the game to finish,
trying to keep themselves entertained by a game in which they are no longer invested. Counter-Strike players may tab out and browse the
internet while waiting for the next round.
Removing a player from a game is extremely
punishing, especially if they have to wait around to play again. Board game designers have realised this, and Euro games have rocketed in popularity, partly due to their "no player elimination" design strategy: rather than giving players resources to lose (such as money or hit points), they award victory to whoever has amassed the most points at the end of the game.
Similarly,
in most first-person shooters, players respawn independently of round timers. For many Valve games, such as Team Fortress 2, instant-respawn mods are extremely popular.
Dying is already punishment enough, and forcing a “time out” for
performing badly just compounds that unnecessarily.
Even some single player games punish the player with a
“game over” screen, forcing them to reload the game, and possibly
even making them replay content. Super Meat Boy solved this issue by making respawning instantaneous—although this was arguably a necessity for SMBs deadly gameplay (where a player can easily die dozens of
times trying a new level), it meant that death never really felt like a punishment.
Awesomenauts also tries to mitigate death. Although the player still has a "time out" period for dying, part of this time out is used playing a mini-game in which the player can collect Solar (the in-game currency). Players can even earn an achievement for playing this mini-game perfectly.
That's not to say that everysingle player game should have instant
respawns, but simply having faster respawns, or the ability
to get straight back into the action after death can really help a
game maintain its flow, rather than breaking momentum with death
screens and “Replay/Quit” menus.
Lockout While Gameplay Unfolds
This
form of lockout is often (but not always) found in turn-based games.
In most JRPG combat systems, players will find themselves able to
perform certain moves and use abilities. Using these abilities will
often enforce a short lockout while the move is executed, after which the
player can continue playing.
While
this sort of turn-based combat makes sense, it doesn't have to be done this way. The Paper Mario series have an
excellent system where choosing attacks in combat opens up a quick time event mini-game. Striking an enemy might deal 10 damage, but if you press the attack button at the correct time during the
animation (at the point where your weapon collides), then you might
deal 20 damage instead.
While
mastering these “stylish moves” wasn't absolutely necessary to
get through the game, it certainly made fights a lot easier, and players
were expected to be able to hit some (if not all) of these moves.
Although
this sort of added interaction is small, it can add up to a large effect
over the course of the game, and it can be applied to a variety of
situations.
Lockout While Waiting for Something to Happen
A
lesser form of lockout occurs in some games when waiting for gameplay to progress. Here, although the player is not directly locked out, any action
the user takes is normally detrimental to winning, so the player is
forced to sit until a favourable position emerges.
Once again,
Counter-Strike is guilty of this, although it is by no means the only
offender. Due to the lethality of the guns, and the one-life-per-round
system, “camping”—that is, hiding for long periods of time in
difficult-to-see areas—is
an incredibly effective tactic. This “sit still, don't move”
approach, while valid, also discourages active gameplay from
participants. Although many Counter-Strike fans will no doubt protest
that camping is part of the tactical gameplay, for new players it can
be frustrating to be killed repeatedly by players that simply sit near
the objective and shoot anyone that moves.
There is no quick fix for
this,
but it is possible to mitigate it by changing certain basic gameplay
elements. The first person shooter Dystopia gives players a "wallhack" ability, where once every 20 seconds they can send a "pulse" that briefly shows them the location of all enemies. Some
Counter-Strike mods damage players who stay in one spot
for too long, forcing them to keep moving or die. These help to a
degree, but they are mostly just small fixes to the larger problem of
one-life gameplay.
Even
outside of games like Counter-Strike, players can find themselves in
situations where they simply have to wait. If a player is waiting for
health to regenerate, or for more resources to be gathered by
workers, then they're effectively locked out. This doesn't mean that you should give the
player everything instantly, but if they find themselves unable to
do anything until the peasants gather enough gold for a castle, then
they're not able to spend their time constructively.
But Waiting Can Be Good
There
are times, of course, when it is good to make a player wait. Take the
famous ladder scene from Metal Gear Solid 3, where the
protagonist literally climbs a ladder for two minutes:
This, on the surface, seems like terrible game design. What would possess the designers to insert a mandatory two minute wait? The purpose is one of storytelling and pacing: the ladder comes after a hectic boss fight, and forcing the player to climb the ladder allows them to transition into a place of calm. Two minutes of not being attacked by enemies, two minutes of reflecting on the battle that took place, two minutes of being allowed a break from the chaos.
As with almost all aspects of games design, you're allowed to break the rules, but its important to understand why you're breaking them.
Lockout During the Opponent's Turn
Locking
players out of a game during their opponents' turns is, not surprisingly, a staple of turn-based
games. In chess, a player is essentially locked out the game until
his opponent moves. Players may be able to consider further options,
but waiting for opponents to take their turn may leave them bored.
There
are ways to mitigate this: chess has a popular variant known as speed chess, where each player is given a certain total amount of time
(say, five minutes each), and must finish the game within that
period. If a player runs out of time, they lose. And while speed
chess is not popular with all chess players—partly because it
does not allow for the same sort of long-term tactical planning
as normal chess games—it does have its fans.
The
problem with chess is that the game is complex. It takes time for a player to consider all the
options available, especially when they start planning
ahead. If you look at a game like noughts and crosses, you'll find
that the gameplay is much faster. This is partly because noughts and
crosses is a simpler game, but more due to the fact that there are
limited options. In chess, the first move a player makes can be one
of 18 possible options. In noughts and crosses, half that. In chess,
as the game progresses, the player will likely find themselves with
more and more options; in noughts and crosses, their options rapidly diminish.
Of
course, it'd be strange to say that limiting a player's options
automatically makes for a better game, as it's the choices we make
within games that make things interesting. But if we ensure that the
players aren't overwhelmed by choices, we can try to ensure that gameplay
is kept relatively fast. Draughts (or checkers) finds itself in a middle-ground between chess and noughts and crosses; it's complex enough to provide an intellectual challenge, but limited enough that players will rarely have to spend ten minutes
considering a single move. The
player generally has fewer pieces available, and all the
pieces do the same thing anyway (except kings).
Taking Turns Without Waiting
It's
possible to partially solve the "waiting for my turn" issue by
having simultaneous turns. The board game Diplomacy does this: during
each turn, every player writes down their unit moves, then all those
moves are enacted during the final phase. Civilisation also attempted
this, though arguably less successfully: players were able to take their
turns at the same time, but due to the more complex nature of the
game this created some situations where the winner of a battle would
be whoever moved their units first. While there are players
who prefer the faster gameplay, it seems to somewhat destroy the
turn-based nature of the game.
Rather than having a “fastest first” approach to simultaneous turns, it's possible to allow players to still play without having a direct effect on gameplay. Civilisation-style games generally require some heavy micro-management, especially at later levels, and there's no reason player can't deal with this during opponents' turns.
You may want to build military units for an upcoming battle, so you could inspect each of your cities and change their build chain to something more suitable. Similarly, you may want to perform diplomatic actions, or change your government type. These actions do not have to be resolved instantly, but allowing players to chain up these actions for the start of their turn allows them to do something during other players' turns, and also (hopefully) makes their own turns faster.
Allowing players to take actions, or respond to certain events, during other players' turns is a good way to keep their involvement high. If a player is invested in opponents' actions, then they are more likely to remain interested in the game. However, if a player doesn't care what their opponent does, then they lose interest
Let
Them Play
Keep the player playing.
Try
to keep wait times to a minimum, and if the player isn't in direct control,
then at least try and give them actions they can perform to keep
themselves entertained.
When we break these rules, understand why you're breaking
them. The most important thing in any game is fun, and sometimes a break from the action—a "ladder scene"—can be a welcome rest, or can help change the atmosphere. But even when trying changing things around, keeping the player involved in a simple way, such as by pressing X at the right time, will give them a feeling of involvement. Remember, we're making games.
We’re looking for a new Course Producer to drive the content direction of Tuts+ Code Courses, working with a great team of regular instructors to produce videos on a range of web development topics. People love to teach! It's a highly rewarding experience being able to help them do that and as a result allow people to learn new skills and take on new opportunities.
As part of the Tuts+ Editorial Team, you'll have the flexibility of working at home to your own schedule, while working among a team of instructors and editors across timezones. You'll be setting the direction, managing people and projects and keeping an eye on consistency and quality of our video content.
The role will require you to work 40 hours per week. You may also have the opportunity to contribute your own courses as an instructor outside of this.
What We're Looking For
The ideal person to fill this rewarding, flexible role would be involved in the web development industry and enjoy keeping up-to-date with the latest trends. You have a background as a web developer or have been learning web development. You enjoy managing projects and working with others to achieve outcomes.
It's likely that you'll have:
Interest and experience in the web development industry.
Ability to work independently and effectively within a remote team environment.
High level of communication and project management skills.
Ability to solve problems and learn new things as needed.
It's a great bonus if you have:
Experience with screencasting and / or video production.
Familiar with HTML, Markdown and GitHub.
Interested? Here's How to Apply
We're hoping to get the process moving fairly quickly with this role. So if you're thinking of applying please do so by Friday, January 30th (or sooner if possible!). If you have any further questions about this role, please contact code.courses@envato.com.