Civ Clone Demo

An early example of a Civilization clone demo, this program created in Java uses two computer controlled players to display A* weighted pathfinding as well as some simple AI.

The map is created using randomly generated Perlin noise. Different tiles of the map have different movement costs (i.e. movement over mountains is slower than plains) so the pathfinding algorithm needed to find not only the shortest route, but also the most efficient. 

Game Design Document.docx

CivClone - Executable Jar.zip

Eclipse Project


Doodle Nabbers: Farm Edition

Wanting to jump right into creating games, the first game I released was Doodle Nabbers: Farm Edition created in Unity with JavaScript using all hand drawn, crayon art. This project had over 35 original scripts with thousands of lines of code and hundreds of crayon drawings. Being my first, full game, the idea was to learn as much as possible so no premade assets were used on this game. I'm quite proud of this app as it is a fun, colorful game with a background that changes depending on the time of day and progression in the game.


Wave Runner

The next game I released was Wave Runner. This game was created in Unity using C# programming with art assets I had crated myself along with premade, purchased art assets. I'm particularly proud of the variety of beautiful level backgrounds as well as the parallax scrolling script I created from scratch which I talk about more below.


Parallax Scrolling Background Script

I assumed there would be numerous, readily available scripts for background parallax scrolling, but not satisfied with any that I found, I decided to make my own. This C# script duplicates the scrolling elements and continually moves one to the front of the screen once it is no longer visible. I went for versatility with this script so it can be used in wide array of areas. It works with both sprites and/or textures either wider or narrower than the screen, and can be used to create whole elements out of parts (example: the stone wall in the example below was created only using short pillar and wall elements).

A video showing the parallax background script in action. This script can duplicate art assets and fill the width of the screen if necessary (i.e. the stone wall was created using 2 small assets).

//////////////////////////////////// /// Versitile Parallax Scrolling /// Joshua Klim 2016 /// /// Used for parallax scrolling objects in background/forground. /// Child elements can be used to fill the width of a screen (i.e. fences). /// Not completly set up for vertical scrolling (i.e. shmups) /// /// Child based parallax objects need to either start wider than the viewable area /// or also need to have fillWithChildren applied to work properly //////////////////////////////////// using UnityEngine; using System; using System.Collections; using System.Reflection; #if UNITY_EDITOR using UnityEditor; using UnityEditorInternal; #endif public class ParallaxScript : MonoBehaviour { [TooltipAttribute ("Range 0 - 1, 0 does not move, 1 matches camera movement, over 1 moves faster than camera (for forground objects)")] public float scrollSpeed; // speed to scroll the texture [TooltipAttribute ("Allow horizontal scrolling?")] public bool horizontalScroll =true; // determines if the parallax should be Horizontal [TooltipAttribute ("Allow vertical scrolling? (not fully implemented)")] public bool verticalScroll; // determines if the parallax should be Vertical [TooltipAttribute ("Auto scroll (i.e. moving objects like clouds)")] public float extraHorizontalMovement, extraVerticalMovement; // makes image scroll regardless of camera movement [TooltipAttribute ("Are the parallax elements children of this object?")] public bool childBased; // ignore any renderer on this object and only use any child sprites [TooltipAttribute ("Inserts a gap betwen objects, negative means overlapping.")] public float duplicateGap; // the gap between this object and duplicate (can be negative to overlap) [TooltipAttribute ("Inserts a gap betwen each fill child, negative means overlapping.")] public float fillChildGap; [TooltipAttribute ("Fills the height/width with duplicates.")] public bool fillWithChildren; // fill the screen size height/width with duplicates or children if childBased == true (sprite only) [TooltipAttribute ("Simply moves the object.")] public bool simplify; private float xOffsetNum; // for keeping track of extra scrolling private float yOffsetNum; private Camera theCamera; // ref to the camera's transform private Material textureMaterial; // the material that is set to repeat itself private SpriteRenderer spriteRenderer; // the spriteRenderer attached to this object private Vector2 spriteSize; // the size of the sprite, not affected by localScale private bool isSprite; // is the renderer a sprite or mesh private Vector3 topRightCorner, bottomLeftCorner; // the corners of the viewing area private GameObject horzChildObject, vertChildObject; // the duplicate child private int parallaxCount; // keeps track of the number of leapfrogs, used for shifting sprite position private Vector2 startPosition; // keeps track of the staring position so it does not move when the scene starts private SpriteRenderer rightSR, leftSR, topSR, bottomSR;// the spriteRenderer furthest in the specified direction private bool duplicated = false; // has the object been duplicated yet? private float objectWidth, objectHeight; // the width of the entire object, not including the duplicate child private float totalObjectWidth, totalObjectHeight; // total size of the object, including duplicate child if there is one private string scriptName; // useful for possible script name changes private Vector2 textureTileNum; void Start() { scriptName = this.GetType().Name; if(GameObject.FindGameObjectWithTag("MainCamera") != null) theCamera = GameObject.FindGameObjectWithTag("MainCamera").GetComponent(); else Debug.LogError("A camera was not found in " + scriptName + "."); topRightCorner = theCamera.ViewportToWorldPoint(new Vector3(1, 1, theCamera.nearClipPlane)); bottomLeftCorner = theCamera.ViewportToWorldPoint(new Vector3(0, 0, theCamera.nearClipPlane)); startPosition = transform.position; if(!childBased && !simplify && GetComponent() != null) // if there is a sprite renderer on the this object { spriteRenderer = GetComponent(); spriteSize = spriteRenderer.sprite.bounds.size; isSprite = true; objectWidth = spriteSize.x * transform.localScale.x; // why not rightSR.bounds.max.x - leftSR.bounds.min.x? objectHeight = spriteSize.y * transform.localScale.y; TotalParallaxBounds(); if(fillWithChildren) { FillWithChildren(); TotalParallaxBounds(); // reset the width with new children objectWidth = rightSR.bounds.max.x - leftSR.bounds.min.x; objectHeight = topSR.bounds.max.y - bottomSR.bounds.min.y; } } else if(!childBased && gameObject.GetComponent() != null) // mesh/texture object { textureMaterial = gameObject.GetComponent().material; isSprite = false; textureTileNum = textureMaterial.mainTextureScale; } else if(childBased) { TotalParallaxBounds(); objectWidth = rightSR.bounds.max.x - leftSR.bounds.min.x; objectHeight = topSR.bounds.max.y - bottomSR.bounds.min.y; if(fillWithChildren) FillWithChildren(); } else if(simplify) { objectWidth = (topRightCorner.x - bottomLeftCorner.x); startPosition = new Vector2( transform.position.x - theCamera.transform.position.x * (1 - scrollSpeed), transform.position.y); } else Debug.LogError("Either a renderer is not attached or the object is not childBased on the " + scriptName + " for the " + transform.name + " object."); } void Update() { if(isSprite || childBased || simplify) // needs to include childBased as there might not be a spriteRenderer on this object { topRightCorner = theCamera.ViewportToWorldPoint(new Vector3(1, 1, theCamera.nearClipPlane)); bottomLeftCorner = theCamera.ViewportToWorldPoint(new Vector3(0, 0, theCamera.nearClipPlane)); if(horizontalScroll) { float offset; if(fillWithChildren) offset = (theCamera.transform.position.x * scrollSpeed) - (totalObjectWidth/2 * parallaxCount) - (duplicateGap * parallaxCount) + (fillChildGap * parallaxCount) - startPosition.x; else offset = (theCamera.transform.position.x * scrollSpeed) - (objectWidth * parallaxCount) - (duplicateGap * parallaxCount) - startPosition.x; // include just parent xOffsetNum += extraHorizontalMovement; // keep track of any extra movement !!! may cause issues if used with duplicate/child gaps? !!! Vector3 tempVector; tempVector = new Vector3(theCamera.transform.position.x - offset - xOffsetNum, transform.position.y, transform.position.z); transform.position = tempVector; if(!simplify) { if(duplicated) { if(rightSR.bounds.max.x + duplicateGap < topRightCorner.x ) LeapFrog("right"); if(leftSR.bounds.min.x + duplicateGap > bottomLeftCorner.x ) LeapFrog("left"); } else if(!duplicated) { if(rightSR.bounds.max.x + duplicateGap < topRightCorner.x ) Duplicate("right"); if(leftSR.bounds.min.x + duplicateGap > bottomLeftCorner.x ) Duplicate("left"); } } else { if() } } if(verticalScroll) { } } else if(!childBased && gameObject.GetComponent() != null) // MeshRenderer object { transform.position = MeshPosition(); UpdateMesh(); } } void FillWithChildren() { if(horizontalScroll) { while(rightSR.bounds.max.x < topRightCorner.x || leftSR.bounds.min.x > bottomLeftCorner.x) { GameObject newChild; newChild = AddDuplicateChild(false); if(childBased && fillWithChildren) { if(rightSR.bounds.max.x < topRightCorner.x) newChild.transform.position = new Vector3(rightSR.transform.parent.transform.position.x + objectWidth + fillChildGap, transform.position.y, transform.position.z); else if(leftSR.bounds.min.x > bottomLeftCorner.x) newChild.transform.position = new Vector3( leftSR.transform.parent.transform.position.x - objectWidth + fillChildGap, transform.position.y, transform.position.z); } else { if(rightSR.bounds.max.x < topRightCorner.x) newChild.transform.position = new Vector3(rightSR.transform.position.x + objectWidth + fillChildGap, transform.position.y, transform.position.z); else if(leftSR.bounds.min.x > bottomLeftCorner.x) newChild.transform.position = new Vector3( leftSR.transform.position.x - objectWidth + fillChildGap, transform.position.y, transform.position.z); } // fillCount++; TotalParallaxBounds(); // recalculate the new bounds } } if(verticalScroll) { } TotalParallaxBounds(); } void TotalParallaxBounds() { if(!childBased) { topSR = spriteRenderer; bottomSR = spriteRenderer; rightSR = spriteRenderer; leftSR = spriteRenderer; } for(int i = 0; i < transform.childCount; i++) { if(transform.GetChild(i).gameObject.activeSelf) FindBounds(transform.GetChild(i).gameObject); else break; for(int j = 0; j < transform.GetChild(i).transform.childCount; j++) { if(transform.GetChild(i).transform.GetChild(j).gameObject.activeSelf) FindBounds( transform.GetChild(i).transform.GetChild(j).gameObject ); else break; for(int k = 0; k < transform.GetChild(i).transform.GetChild(j).transform.childCount; k++) // for childBased && fillWithChildren if(transform.GetChild(i).transform.GetChild(j).transform.GetChild(k).gameObject.activeSelf) FindBounds( transform.GetChild(i).transform.GetChild(j).transform.GetChild(k).gameObject ); else break; } } totalObjectWidth = rightSR.bounds.max.x - leftSR.bounds.min.x + duplicateGap; totalObjectHeight = topSR.bounds.max.y - bottomSR.bounds.min.y; } void FindBounds(GameObject gameOb) { SpriteRenderer tempSR = new SpriteRenderer(); if(gameOb.GetComponent() != null) tempSR = gameOb.GetComponent(); else if(childBased) tempSR = rightSR; if( childBased || tempSR != null && gameOb.GetComponent(scriptName) != null) // make sure the child has a Sprite and is a Parallax object clone { if( rightSR == null || tempSR.bounds.max.x > rightSR.bounds.max.x) rightSR = tempSR; if( leftSR == null || tempSR.bounds.min.x < leftSR.bounds.min.x) leftSR = tempSR; if( topSR == null || tempSR.bounds.max.y > topSR.bounds.max.y) topSR = tempSR; if(bottomSR == null || tempSR.bounds.min.y < bottomSR.bounds.min.y) bottomSR = tempSR; } } Vector3 MeshPosition() { Vector3 newPosition; if(horizontalScroll && verticalScroll) newPosition = new Vector3(theCamera.transform.position.x, theCamera.transform.position.y, transform.position.z); else if(horizontalScroll) newPosition = new Vector3(theCamera.transform.position.x, transform.position.y, transform.position.z); else if(verticalScroll) newPosition = new Vector3(transform.position.x, theCamera.transform.position.y, transform.position.z); else newPosition = Vector3.zero; return newPosition; } // moves object off the back of the screen to the front void LeapFrog(string direction) { Vector3 newPosition = Vector3.zero; switch (direction) { case "right": newPosition = new Vector3(transform.position.x + (totalObjectWidth / 2), transform.position.y, transform.position.z); parallaxCount++; break; case "left": newPosition = new Vector3(transform.position.x - (totalObjectWidth / 2), transform.position.y, transform.position.z); parallaxCount--; break; case "up": newPosition = new Vector3(transform.position.x, vertChildObject.transform.position.y, transform.position.z); break; case "down": newPosition = new Vector3(transform.position.x, -vertChildObject.transform.position.y, transform.position.z); break; } transform.position = newPosition; // for when the parent is not at (0,0) TotalParallaxBounds(); } void UpdateMesh() { float xOffset = 0f; float yOffset = 0f; if(horizontalScroll) xOffset = theCamera.transform.localPosition.x * scrollSpeed / (transform.localScale.x / textureTileNum.x); if(verticalScroll) yOffset = theCamera.transform.localPosition.y * scrollSpeed / (transform.localScale.x / textureTileNum.y); xOffsetNum = StepLimit(xOffsetNum + (extraHorizontalMovement / (transform.localScale.x / textureTileNum.x))); yOffsetNum = StepLimit(yOffsetNum + (extraVerticalMovement / (transform.localScale.x / textureTileNum.y))); Vector2 totalOffset = new Vector2( xOffset + xOffsetNum, yOffset + yOffsetNum); textureMaterial.SetTextureOffset("_MainTex", totalOffset); } Vector3 NextPosition() { if(horizontalScroll) return new Vector3(spriteSize.x, 0, 0); else if(verticalScroll) return new Vector3(0, spriteSize.y, 0); else return Vector3.zero; } private float StepLimit(float num) // keeps number constantly cycling between 1 and -1 { if (num >= 1) return num - 1; if (num <= -1) return num + 1; return num; } void Duplicate(string direction) { if(isSprite || childBased) { Vector3 tempPos = Vector3.zero; switch (direction) { case "right": horzChildObject = AddDuplicateChild(true); tempPos = new Vector3( (totalObjectWidth / transform.lossyScale.x), 0, 0); horzChildObject.transform.localPosition = tempPos; break; case "left": horzChildObject = AddDuplicateChild(true); tempPos = new Vector3( (-totalObjectWidth / transform.lossyScale.x), 0, 0); horzChildObject.transform.localPosition = tempPos; break; case "up": vertChildObject = AddDuplicateChild(true); break; case "down": vertChildObject = AddDuplicateChild(true); break; } duplicated = true; } else { Debug.LogError("Error duplicating object in " + scriptName + "."); } TotalParallaxBounds(); } GameObject AddDuplicateChild(bool duplicateEntireObject) { GameObject newChild = Instantiate(gameObject, this.transform) as GameObject; if(newChild.GetComponent(scriptName) != null) newChild.GetComponent().enabled = false; newChild.transform.localScale = Vector3.one; if(!duplicateEntireObject) { newChild.name = this.name + "FillChild"; for(int i = 0; i < newChild.transform.childCount; i++) { GameObject tempGO = newChild.transform.GetChild(i).gameObject; if(tempGO.GetComponent(scriptName) != null) { tempGO.SetActive(false); // neccessary as Destroy does not remove the gameObject until the next Update frame Destroy(tempGO); } } } else { newChild.name = this.name + "Duplicate"; } return newChild; } }

Upcoming Game:

Viking Graveyard (working title)

The player can earn coins by defeating enemies to buy upgrades such as better bows, flame arrows, stronger cemetery gates, etc.. The current pre-alpha state does not display the level progression, bosses, store or many other features. This game will feature much more mecanim animation than previous games.

Pre-alpha demo APK file can be downloaded here

A brief demo of the upcoming game Viking Graveyard (working title). This game is still in pre-alpha stage and does not yet contain level progression or many other features.