Move around with the arrow keys. An experiment with surfaces. The world is broken up into tiles which are rendered to surfaces so that we only have to re-draw the tiles when something changes. No real gameplay but surfaces are a powerful tool so well worth starting to learn them.
1HGJ #001: Telekinetic Bomberman Rush
Click anywhere to place a bomb (or, destroy a wall). Use arrow keys to guide the bomb around the level. Bombs explode and destroy walls.
Sprites and animation, keyboard interactions, timings.
Ran out of time before having an actual win/lose state so this is more of a prototype of a single mechanic than a game.
Nonoku - SpriteKit Shader
A quick one because I was having trouble with this:
let node = SKShapeNode(rectOf: CGSize(width: 100, height: 100), cornerRadius: 5)
let dashedShader = SKShader(source: "void main() {" +
"float diff = u_path_length / 5.0;" +
"float stripe = u_path_length / diff;" +
"float distance = v_path_distance;" +
"int h = int(mod(distance / stripe, 2.0));" +
"gl_FragColor = vec4(h, h, h, h);" +
"}")
node.strokeShader = dashedShader
node.fillColor = SKColor.clear
node.lineWidth = 2
I've not done much with shaders before so when it didn't work I was short on tools to debug it. In the end it was a bunch of little things I had to solve, from converting the original version to one that would work against the iOS GL ES version, to making sure I converted to int before setting the colour.
Nonoku - Old is New
I’ve finally buckled down and brought the old levels over so I’ve got a nice set of test data to validate against. In the end all it took was a couple of regex statements and a little copy/pasting.
Nonoku - Level Select Selections
Updated the Level Select screen to handle touch interactions. It’s now possible to tap on a puzzle image and be taken to that puzzle. I ended up using the userData: NSMutableDictionary
property on the SKNode
to both determine if the node the user tapped represents a puzzle, but also, which puzzle they tapped.
private func nodes(touches: Set<UITouch>) -> [SKNode]? {
for touch in touches {
let location = touch.location(in: currentScene)
nodesTouched = currentScene.nodes(at: location)
return nodesTouched.filter {
guard let _ = $0.userData?[“puzzleName”] else { return false }
return true
}
}
return nil
}
Nonoku - Level Select Update
Another small update. I’ve got three puzzles converted over from the previous game. I’m changing the format (and fixing some issues at the same time with how colours were handled). As a result, it’s a little time-consuming to get them all ported over. I’ll probably write a quick tool to convert the remainder.
Nonoku - Composition
Since I’m using the same background in a bunch of places I want to make it really easy to re-use. I could make a sub-class of UIViewController which implements the functionality. I’m not a fan of that, if it can be avoided. These things have tendency to grow. I could also create a static class method to return the background SKNode. That’s a little better but that’s just a form of composition and Swift actually gives us a nice way to implement this with Protcols
and Extensions
.
I start by moving declaring the function used for creating the background in a protocol. I’ll call it BackgroundProtocol
because I’m awful at names. That’s easy to change later if I decide to add more functionality anyway.
protocol BackgroundProtocol {
func createBackground(size: CGSize) -> SKNode
}
I can add that protocol to other classes and they’ll have the createBackground(size:)
function available to them. Using an extension I can then create a default implementation (by striking coincidence, that’s the code I already had for this).
extension BackgroundProtocol {
func createBackground(size: CGSize) -> SKNode {
let node = SKEffectNode()
// snip ...
return node
}
}
Now I can add that to all the views in my app and they can just call it to get the functionality. And, of course, it can be overridden if needed.
class MainMenuViewController: UIViewController, BackgroundProtocol {
override func viewDidLoad() {
super.viewDidLoad()
let spriteView = (view as? SKView)!
let scene = SKScene(size: view.bounds.size)
scene.addChild(createBackground(size: scene.size))
spriteView.presentScene(scene)
}
}
Nonoku - Minor Tweaks
Just a quick one. Added a black fill colour with low opacity to the board to give it a little bit of depth and separation from the background.
Nonoku - Creating a Background
So, I was getting bored of looking at a grey background and, as a break from cleaning up, refactoring, and adding the fiddly (nut necessary) UI code, I decided to run up a quick background.
The original version of Nonoku had a vertical gradient fill, light to dark, with subtle diagonal striping. I liked this so decided to reproduce it. However, it made sense to me to implement this in code. Since I’m using SpriteKit
already, rendering to an SKEffectNode
which rasterises the nodes made sense (the background is not animated so I can basically update once and be done).
There’s no native way, as far as I’m aware, to draw a gradient on a node, so the first thing would be to create an SKTexture
. Again, since this is a one time deal, I’m not too worried. I’ve done something similar before so I already knew I could use a CIFilter to generate this. From that, I can get a CIImage
. That can be passed to a CIContext
to create a CGImage
which can finally be passed to the SKTexture
which I’ll pass into an SKSprite
.
From reviewing the CIFilter
documentation, I can get a list of available filters as follows:
class func filterNames(inCategory category: String?) -> [String]
Even better, under the list of category constants, there is kCICategoryGradient
. For this kind of quick exploratory code, I usually start a Swift Playground to spike out the things I don’t know.
So, skipping a step, I have this:
print("\(CIFilter.filterNames(inCategory: kCICategoryGradient))")
let filter = CIFilter(name: "CILinearGradient")
print("\(filter?.attributes)")
The list of filterNames has a couple of likely contenders, CILinearGradient
and CISmoothLinearGradient
. They take the same attributes (two colours, and two vectors) so I ended up trying both. In my use case I couldn’t see any difference between the two so decided to stick with CILinearGradient
. If it ever comes up as an issue, it’s a very simple change to make.
CIFilter.attributes()
gives me a list of the supported attributes, and a brief description of them. That's enough to define how I'm going to use it so I can leave the playground and come back to my code.
Since I want to create a new SKTexture
with this gradient it makes sense to do it as an extension on SKTexture
. I need the size, the start colour, and the end colour. I could add additional logic here but, following YAGNI (You Ain’t Gonna Need It), I am only interested in a vertical gradient so that’s all I’ll support.
extension SKTexture {
convenience init?(size: CGSize, color1: SKColor, color2: SKColor) {
guard let filter = CIFilter(name: "CILinearGradient") else {
return nil
}
let startVector = CIVector(x: 0, y: size.height)
let endVector = CIVector(x: 0, y: 0)
filter.setValue(startVector, forKey: "inputPoint0")
filter.setValue(endVector, forKey: "inputPoint1")
filter.setValue(CIColor(color: color1), forKey: "inputColor0")
filter.setValue(CIColor(color: color2), forKey: "inputColor1")
let context = CIContext(options: nil)
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
guard let filterImage = filter.outputImage,
let image = context.createCGImage(filterImage, from: rect) else {
return nil
}
self.init(cgImage: image)
}
}
Interestingly, whilst I can just get a ciColor
from a UIColor
(SKColor
aliases UIColor
), I can’t use it in the CIFilter
due to the following issue.
*** -CIColor not defined for the UIColor UIExtendedSRGBColorSpace 0.666667 0.223529 0.223529 1; need to first convert colorspace.
Instead, I had to create a new instance of CIColor
with the UIColor
. I’m not sure if there are any issues associated with this, but it’s working fine for me so far.
From there, I just created an SKSpriteNode
with the generated texture. The lines were just stroked SKShapeNodes
with a CGPath
which just have a defined start and end point.
So here’s how it looks:
Nonoku - Scrolling Level Select
Combining an SKNode
with a UIScrollView
. These are two things which don’t go together natively. However, I wanted a scrolling view. I didn’t want to re-implement the UIScrollView
in SpriteKit
. I also didn’t want to have to handle rendering the puzzle to a UIImage
and create UIImageViews
when I already had a perfectly good method for building an SKNode
from the tiles. A little modifying to let me add it to an SKEffectNode
and I had my image. Create a new SKScene
and present it in my view and I have part of the solution.
Adding it to the UIScrollView
isn’t possible, but getting the offset of the scrollview is so if I tell it the content is larger than the screen. Say, large enough to fit the grid of puzzle images, I could attach those images to a SKNode
and move it by the offset.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
gridNode.position.x = -scrollView.contentOffset.x
gridNode.postion.y = scrollView.contentOffset.x
}
}
I only care about horizontal movement. The gridNode is positioned at 0,0
in the scene. Horizontal movement is inverted, relative to the offset. Vertical movement is not because the SpriteKit
coordinate system is inverted vertically compared to the UIView
coordinates. In SpriteKit
, 0,0 is in the bottom-left of the screen and increasing the y-coord moves it towards the top, UIView
would put it in the top-left and increasing the y-coord moves it towards the bottom.
Since the scene is sitting behind the components I still get the UIScrollView
scroll bars, and any other controls I might add and now I don’t have to re-implement the UIScrollView
.
Next up, passing a touch event through to the scene.
Nonoku - Taking Shape
It's now possible to select areas and fill them in based on the colour selected. Incorrectly coloured tiles will not be revealed. This feels like a very big step and, ultimately, it all fell into place in just a few lines of code.
Looking back, I've clearly grown as a developer since I wrote the first version. Whilst I've made decisions that simplify the interface, I've also implemented a lot of the functionality that caused me a lot of pain before in just a few lines now. Part of that is making better technical decisions up front, another part is using the right tools. Another big piece is just having more experience in more areas so I have more ideas of what is going to be available to me before I start coding.
Overall, I'm happy with the progress so far and I'm glad I'm making the time to go back update an old project.
Nonoku - A Quick Update
Trying to keep things going as and when I can. This is a super quick-update just to display the correct buttons for the current puzzle. I know I'll be swamped for the next few days so grabbing 15 mins just to get another little piece done is going to be vital.
Nonoku- Selecting Things
Game Jam: Werewolves of War
Unity - Generating Builds
Adding the below code to Assets/Editor will add an entry to the menu bar that allows me to generate two builds automatically. Since they're also placed in a folder with the day's date, I also get build histories for the work done. For GameJams this is ideal as I'll usually only get a small amount of time on an evening to work on these so I'm not doing much more (and I could always use a more precise timestamp if needed). This is basic but so easy to automate and a nice little time saver that I thought I'd share.
using UnityEditor; using System.Diagnostics; using System; public class JamBuilds { [MenuItem("Build/Jam Builds")] public static void BuildGame () { // Get filename. string path = EditorUtility.SaveFolderPanel("Choose Location of Built Game", "", ""); string[] levels = new string[] {"Assets/Scenes/Test.unity", "Assets/Scenes/Finale.unity"}; string projectName = PlayerSettings.productName; string date = DateTime.Now.ToString("/yyyy-MM-dd"); // Build player. BuildPipeline.BuildPlayer(levels, path + date + "/" + projectName + ".exe", BuildTarget.StandaloneWindows, BuildOptions.None); // Build player. BuildPipeline.BuildPlayer(levels, path + date + "/WebPlayer", BuildTarget.WebPlayer, BuildOptions.None); } }
An Evening of Code: Elevator
A distraction. Took a few minutes just to implement a basic mechanic. Not sure what this is to do with anything but into the Experiments/ dir it goes.
The elevator has a collision box and, when the player touches it we can activate the 'elevator' functionality on a keypress. The system allows up and down movement so you could have multiple floors.
An Evening of Code: Generating Roads 2
Fixed a couple of bugs which was keeping the generation from truly finishing which makes increasing the frequency of the branches more feasible. I tweaked the previous generation to increase how often it branched as it approaches the centre. It's still way to messy and chaotic to be a workable solution but for something so basic and naive it actually does better than I expected.
An Evening of Code: Sierpinski Triangle
An extension of yesterday's work with L-Systems. The Sierpinski Triangle.
Firstly, I updated the implementation to draw the shape over a number of frames. This is not the end goal of this work so I'm doing it in Unity by drawing to a texture which is easy to implement but far from the most efficient ;) In this case it was best to limit the texture size to 512x512 to avoid a severe performance hit when redrawing the texture. If I needed to, I could simulate a larger texture by mapping it to multiple smaller ones but that's out of scope for the moment.
Next I updated the rules implementation to support multiple rules. Previously I used a single string and a regular expression. This no longer worked because the rules have to be applied simultaneously. Instead, I used a Dictionary to store the rules and, since the rules I need at the moment are only applied to a single character I could simply enumerate the axiom and use the character as the key to the dictionary. If there's a match I append the rule to a new string. If not I append the character.
There is one additional change that is not described in the notation - after each iteration the angle gets flipped so that it goes from 60 to -60, and back again. This is required to keep the triangle facing the same way at each iteration.
A= Step forward
B = Step forward
+ = +60 degrees
- = -60 degrees
A → B-A-B
B → A+B+A
Axiom: A
After 9 iterations the string has gone from 1 to 39,365 characters.
string Iterate(string axiom) { string newAxiom = ""; foreach (char c in axiom) { string ch = c.ToString(); string replace = null; rules.TryGetValue(ch, out replace); if (replace != null) { newAxiom += replace; } else { newAxiom += ch; } } angle = -angle; Debug.Log ("Axiom Length: " + newAxiom.Length); return newAxiom; } void FixedUpdate() { int length = axiom.Length; for (int i = 0; i < 200; i++) { if (index < axiom.Length) { char c = axiom[index]; if (c == 'F' || c == 'A' || c == 'B') { Vector2 newPoint = NewPoint(currentPoint, stepLength, angle); DrawLine (new Line(currentPoint, newPoint), colors[colIndex % 3]); colIndex++; currentPoint = newPoint; } else if (c == '-') { angle -= angleStep; } else if (c == '+') { angle += angleStep; } index++; } } texture2D.Apply(); }
An Evening of Code: Koch Snowflake
The Koch Snowflake is the one of the first, and simplest, of the fractals.
Starting with an equilateral triangle:
- Start at the first line and remove it.
- Set the step length by dividing the line length by 3.
- Set the angle to that of the line.
- Step forward.
- Rotate 60 degrees left.
- Step forward.
- Rotate 120 degrees right.
- Step forward.
- Rotate 60 degrees left.
- Step forward.
In total, we've added four new lines and increased the total length of the shape by 1/3. Repeat for the remaining lines and we have completed an iteration. Starting again on the new shape we can repeat the same steps for each of the new lines. As can be seen from the above animation, which only iterates 5 times, the complexity rapidly increases.
Technically this is not actually a Koch Snowflake as that is the shape we approach as we iterate. However, we would need to iterate an infinite number of times to generate a true Snowflake.
Iterating on a single line generates a Koch Curve. The Snowflake, starting from a triangle, is made of three curves.
The Koch Curve can be described by a Lindenmayer (L-) System. This is where we create a string and, each iteration, run a set of rules to replace characters in the string with others. Each character defines a drawing rule which we can follow to draw the shape after the iterations have completed.
F = Step Forward
+ = Rotate 60 degrees right
- = Rotate 60 degrees left
Rules
F → F-F++F-F
Starting with: F++F++F++
Our drawing instructions give us our starting triangle. Iterating, we increase the size of the string and generate more rules. Following them generate the animation above.
float stepLength = 256; string rule = "F-F++F-F"; Vector2 start = new Vector2(128, 128); protected override void Draw () { string axiom = "F++F++F++"; for (int i = 0; i < 5; i++) { axiom = Iterate(axiom); } int index = 0; Color[] colors = new Color[3]{Color.black, Color.red, Color.blue}; int length = axiom.Length; Vector2 currentPoint = start; float angle = 0; int colIndex = 0; while(index < axiom.Length) { char c = axiom[index]; if (c == 'F') { Vector2 newPoint = NewPoint(currentPoint, stepLength, angle); DrawLine (new Line(currentPoint, newPoint), colors[colIndex % 3]); colIndex++; currentPoint = newPoint; } else if (c == '-') { angle -= 60; } else if (c == '+') { angle += 60; } index++; } } string Iterate(string axiom) { Regex regex = new Regex("F"); stepLength /= 3; return regex.Replace(axiom, rule); } Vector2 NewPoint(Vector2 point, float step, float angle) { float radAngle = angle * Mathf.Deg2Rad; float x1 = (point.x + (step * Mathf.Cos(radAngle))); float y1 = (point.y + (step * Mathf.Sin(radAngle))); return new Vector2(x1, y1); }
Try modifying the above to generate new shapes. For example, replacing the '+' with '-' symbols in the starting axiom will create the equilateral triangle facing in the other direction and which starts a different pattern which will never exceed the bounds of the initial equilateral triangle.
An Evening of Code: Drawing a Line
Not hugely exciting but I did also spend a little time cleaning up an old project. Didn't end up doing as much as I thought was needed so instead I started looking at ways to start learning L-systems. To begin, I'll need a way to get them on the screen. For the first go I figured being able to draw lines to a texture would be sufficient. This is a quick and dirty implementation of Bresenham's line algorithm. I don't need it to be fast so substituted floats for the bit shifting just to make it easy. Nothing to be proud of here but it does get me what I need for tonight.
public void DrawLine(int x0, int y0, int x1, int y1, Color color) { int dy = y1 - y0; int dx = x1 - x0; float error = 0; float deltaError = 0; if (dx != 0 && dy != 0) { deltaError = Mathf.Abs((float)dy / (float)dx); } int y = y0; int x = x0; for (int i = 0; i < Mathf.Abs(dx); i++) { texture2D.SetPixel(x, y, color); error = error + deltaError; while (error >= 0.5f) { texture2D.SetPixel (x, y, color); y += Mathf.RoundToInt(Mathf.Sign(dy)); error = error - 1.0f; } x += Mathf.RoundToInt(Mathf.Sign(dx)); } }