Christopher Poole's Portfolio
Programming
Programming is the backbone to any project and in my game development exploits I've honed these skills considerably. Click on a section below to learn how I applied specific aspects of programming in my games. The following is all programmed in the Unity engine with C# but I also know how to program in many other languages including staples like C++ and Java. I also removed some lines of the code for clarity (for example, code that plays sound effects or edits variables the section isn't talking about).
Object Oriented Programming seems like a good place to start. I will reference the following code I wrote for my mobile game Synegraph through the lense of different OOP concepts:
public class VisualCardCollection : MonoBehaviour, IDropHandler { [SerializeField] private State state; private Listcards; //The base function for AddCard does all the heavy lifting, adding the card to the arrays //required and updating its information so it views this object as its owner. public virtual void AddCard(Card newCard) { cards.Add(newInst.GetCard()); newCard.SetVisualCardCollection(this); newCard.transform.SetParent(transform, false); } public void OnDrop(PointerEventData eventData) { if (CardInstance.dragging != null && !IsLocked() && !PlayerManager.instance.IsAnimating()) { AddCard(CardInstance.dragging); } } } public class Board : VisualCardCollection { //Board needs to override AddCard because changes to the board //need to trigger a visual update on the Talisman mechanic. public override void AddCard(Card newCard) { base.AddCard(newCard); myVisualTalismanCollection.UpdateTalismanHighlights(this); } } public class Deck : VisualCardCollection { //Deck doesn't override AddCard, but instead creates a new function DrawCard //as well as some other functions for saving a copy of itself. public Card DrawCard() { if (GetCardCount() > 1) { int cardIndex = Random.Range(0, GetCardCount()); Card drawnCard = GetCards()[cardIndex]; RemoveCardAt(cardIndex); return drawnCard; } } }
In my mobile game, Synegraph this is best demonstrated by how UpdateTalismanHighlights is called. This function checks the board state to see if a Talisman is activated and thus needs its activated visual turned on. Each Talisman.IsActive(board) function can be up to O(n) complexity and this is done O(n) times so the total complexity of the function is O(n^2). Fortunately I know based on the rules of the game that the player wouldn't have that many cards or Talismans so n is a low number. Still, I don't want to be calling this on Update where its possible the board state hasn't even changed. The result is this code:
public class VisualTalismanCollection : MonoBehaviour { public void UpdateTalismanHighlights(Board board) { for (int i = 0; i < talismans.Count; i++) { if (board != null) { bool isActive = talismans[i].IsActive(board); talismanHighlightGameObject.SetActive(isActive); } else { talismanHighlightGameObject.SetActive(false);//if we sent a null board just turn them all off } } } } public class Board : VisualCardCollection { public override void AddCard(CardInstance newInst) { base.AddCard(newInst); myVisualTalismanCollection.UpdateTalismanHighlights(this); } } public class VisualCardCollection : MonoBehaviour, IDropHandler { public virtual void AddCard(CardInstance newInst) { cards.Add(newInst.GetCard()); instances.Add(newInst); newInst.SetVisualCardCollection(this); newInst.transform.SetParent(transform, false); } public void OnDrop(PointerEventData eventData) { if (CardInstance.dragging != null && !IsLocked() && !PlayerManager.instance.IsAnimating()) { AddCard(CardInstance.dragging); } } }
This is an example of a Race Data file and Ability Data file. The "ability" field in the Race Data file acts like a link to the Ability Data file. This allows designers to build everything seperately and then link them together with only unique names.
name: Dragonkin stats: { maxHp: 1.5 power: 1.5 resistances: { incoming chaos -20 incoming sharp -20 } } ability: Fire Breath portrait: dragonkin /////////////////////// name: Fire Breath cooldown: 4 delay: 0 free_action: false targets: { front all (threat) } use_code: { damage target1 0.7 chaos } description: { auto generate } icon: fire breath projectile: { none } hitAnim: { ring of fire }
The "use_code" portion of an ability is the most powerful. This is essentially a highly simplified programming language that is intuitive to use. The line "damage target1 0.7 chaos" means deal chaos damage at 0.7 times your attack power to the first target listed under "targets". This "use_code" can be multiple lines and include stuff like "apply_status target2 Burn" and "heal target1 0.5". The targets can also be complex ranging from enemy frontlines to ally backlines. Having "(threat)" indicates the attack will primarily be used against enemies, but you can enable friendly fire in the game to target allies as well.
public class PlayerCharacter : Character, IDragHandler, IBeginDragHandler, IEndDragHandler { private void randomize() { //In this case we're choosing a random race, so we choose a random file from the "Races" directory. race = new Race(FileInterpreter.getRandomFileInDirectory("Data/Races/", ".txt"));//Finding the text file. //Removed rest of the code about loading other aspects of the PlayerCharacter for clarity sake. } } public class Race : Stats { public Race(string filepath) { try { StreamReader sr = new StreamReader(filepath); string fullText = sr.ReadToEnd(); loadStatsFromText(fullText); //It also loads its name and icon here but I cut that out for clarity. sr.Close(); } catch (FileNotFoundException fnfe) { Debug.Log(fnfe); } } } public class Stats { public void loadStatsFromText(string text) { maxHp = FileInterpreter.getFloatValue(text, "maxHp", 0);//Reading the text file pwr = FileInterpreter.getFloatValue(text, "power", 0); updateResistancesFromString(FileInterpreter.getStringValue(text, "resistances", "")); } } public class FileInterpreter { //----------------------------------------- //-----------DIRECTORY SEARCHING----------- (These headers are copy pasted from the code. //----------------------------------------- I take organization seriously, especially in a big class like this.) public static string [] getAllFilesInDirectory(string path, string fileType, bool getFullPath) { string[] fileNames = Directory.GetFiles(resourceFile+"/"+path, "*"+fileType, SearchOption.AllDirectories); if (!getFullPath) { for (int i = 0; i < fileNames.Length; i++) { int lastSlash = fileNames[i].LastIndexOf('/'); fileNames[i] = fileNames[i].Substring(lastSlash); } } return fileNames; } public static string getRandomFileInDirectory(string path, string fileType) { System.Random rand = new System.Random(); string[] files = FileInterpreter.getAllFilesInDirectory(path, fileType, true); return files[rand.Next(files.Length)]; } //---------------------------------------------- //-----------GETTING VALUES FROM FILE----------- //---------------------------------------------- public static float getFloatValue(string fullText, string property, float defaultValue = 0.0f) { property = property + ":"; if (!fullText.Contains(property)) return defaultValue; else { string[] splitText = fullText.Split('\n'); trimStringArray(splitText);//Helper function string line = splitText[findPropertyLine(splitText, property)];//Helper function string value = getLinePropertyStringValue(line);//Helper function value = value.Trim(); float i = defaultValue;//i don't know I think I need like 100 more checks? try { i = float.Parse(value); } catch (FormatException) { return defaultValue; } catch (OverflowException) { return 1000000000f;//this is returned when the value is below -2147483647 or above +2147483647 } return i; } } //-------------------------------------------- //-----------MISC HELPERS FOR ABOVE----------- //-------------------------------------------- public static void trimStringArray(string[] array) { for (int i = 0; i < array.Length; i++) { array[i] = array[i].Trim(); array[i] = array[i].Trim('\t'); } } public static int findPropertyLine(string[] splitText, string property) { for (int i = 0; i < splitText.Length; i++) { if (splitText[i].Contains(property)) return i; } return -1; } public static string getLinePropertyStringValue(string line) { if (line.Contains(":")) { int colon = line.IndexOf(":"); return line.Substring(colon+1, line.Length - colon - 1);//return everything until the end of the line } else return ""; } }
//uploads a text file with the name "name" and the text "text" to the default file directory for the selected canvas course. public IEnumerator CreateNewTextFile(string name, string text) { if (!name.Substring(name.Length - System.Math.Min(4, name.Length)).Equals(".txt")) { name += ".txt";//if we don't have the suffix we've got to add it } //The following steps reference the Canvas API documentation at: https://canvas.instructure.com/doc/api/file.file_uploads.html //------------------------------------------------------------- //STEP 1: Send our first APICall and get back our upload_params //------------------------------------------------------------- PrintDebugMessage("FILE UPLOAD", "Creating Text File - Name: " + name + ", Folder Path: " + gameFolderPath); Listargs = new List (); args.Add("name=" + name); args.Add("size=" + text.Length);//size in bytes args.Add("content_type=text/plain"); args.Add("parent_folder_path=" + gameFolderPath); args.Add("on_duplicate=overwrite");//if the file already exists, overwrite it //post that data to the files directory CoroutineWithData cd = new CoroutineWithData(this, CanvasAPIManager.instance.APICall("post", "courses", "auto", "files", args.ToArray())); yield return cd.wait; JSONNode tempNode = JHandler.parseNodeFromRaw(cd.data.ToString());//get back the response from the Canvas server string uploadURL = tempNode["upload_url"].ToString().Trim('\"');//grab the upload_url from the response //-------------------------------------------------------------------------------------------------------------- //STEP 2: Take the upload_params and send a special API call to the destination URL returned from the first call //-------------------------------------------------------------------------------------------------------------- //send back all the upload_params we just received string[] uploadParams = JHandler.SplitKeyValuePairs(tempNode["upload_params"].ToString()); WWWForm form = new WWWForm();//we're creating a form here because normally CanvasAPIManager.instance.APICall does that for us for (int i = 0; i < uploadParams.Length; i = i + 2) { form.AddField(uploadParams[i], uploadParams[i + 1]); } //convert the text string to bytes and send it as the last argument byte[] bytes = Encoding.ASCII.GetBytes(text); form.AddBinaryData("file", bytes); UnityWebRequest www2 = UnityWebRequest.Post(uploadURL, form);//second request, this time just doing it directly not via ApiCall b/c this is special www2.redirectLimit = 0;//we DON'T want Unity to redirect for us yield return www2.SendWebRequest(); //------------------------------------------------------------------------------------------ //STEP 3: Parse the response we got from the second API call and possibly send a 3rd request //------------------------------------------------------------------------------------------ if (www2.responseCode.ToString()[0] == '3') { //if we get a 3XX redirect, we need to POST to that location or the file won't be availible string redirectLocation = www2.GetResponseHeader("Location").ToString();//get the location from www2's response header UnityWebRequest www3 = UnityWebRequest.Post(redirectLocation, "access_token=" + accessToken);//send verification yield return www3.SendWebRequest(); PrintDebugMessage("FILE UPLOAD", "Successful upload!"); } else if (www2.responseCode == 201) { //success but don't have to verify PrintDebugMessage("FILE UPLOAD", "Successful upload!"); } else { PrintDebugMessage("FILE ERROR", "File upload error! HTTP Error Code: " + www2.responseCode); } yield return null; }
This is my algorithm for determining if there are X1 of element Y1 on the board as well as X2 of element Y2, X3 of element Y3 and so on. Its important it scales because this one script is used to create assets for many different Talismans all with different element requirements.
[SerializeField] private int [] amounts;//Amounts we need to fight for each element. Corresponds to the array of elements [SerializeField] private Element [] elements;//Elements we need to find. Corresponds to the array of amounts public override bool IsActive(Board board) { int [] numbs = new int[elements.Length]; //create a 3rd array also corresponding to the elements array. //This time its the actual number of elements we found on the board. foreach (Card card in board.GetCards())//loop through all the cards on the board { for (int i = 0; i < elements.Length; i++)//check all the elements we're searching for... { if (card.GetElement() == elements[i])//...to detemine if the current card we're looking at is one of those elements. numbs[i]++;//if it is, increment that value. } } for (int i = 0; i < elements.Length; i++)//to check if we found everything, loop through the elements we're looking for { if (amounts[i] == 0 && numbs[i] != 0)//this is a special case: if 0 is given as the amount it means we don't want ANY of that element on the board return false; else if (numbs[i] < amounts[i])//if we find even one element that didn't reach its quota, return false and save us from looping the last bit. return false; } return true;//if we didn't fail, we succeed. }
This is my algorithm for determining if there are "amount" number of the same value on the board:
[SerializeField] private int amount;//number of cards of the same value public override bool IsActive(Board board) { int numb;//actual number of cards of the same value found Listcards = board.GetCards(); for (int j = 0; j = amount) return true;//if we found it enough of that card, return true right away to save us from looping the rest of the cards. } return false;//if we didn't succeed, we fail. }
The following code is an exert of the full Custom Editor that handles drawing the waypoints and letting you interact with them.
void OnSceneGUI() { serializedObject.Update(); int controlID = GUIUtility.GetControlID(FocusType.Passive);//not 100% on this, but I think this sets up our control id //DRAW THE VISUAL REPRESENTATION OF THE WAYPOINTS IN THE SCENE for (int w = 0; w < mr.waypoints.Count; w++) { if (mr.waypoints.Count >= 2)//draw lines between intermediary waypoints { if (lastPos != Vector3.zero) { if (!isLineOnGrid(mr.waypoints[w].pos, lastPos)) Handles.color = GlobalColors.red; Handles.DrawLine(mr.waypoints[w].pos, lastPos); } lastPos = mr.waypoints[w].pos; } Handles.color = GlobalColors.colorColors[mr.waypoints[w].color]; Handles.DrawSolidDisc(mr.waypoints[w].pos, Vector3.forward, 0.2f); Handles.Label(mr.waypoints[w].pos, w.ToString(),style); } lastPos = Vector3.zero; if (mr.waypoints.Count >= 1) { if (!isLineOnGrid(mr.waypoints[0].pos, mr.waypoints[mr.waypoints.Count - 1].pos)) Handles.color = GlobalColors.red; Handles.DrawLine(mr.waypoints[0].pos, mr.waypoints[mr.waypoints.Count - 1].pos);//draw a line between the first and last } //HANDLE MOUSE EVENTS if (choosingPosition) { Handles.DrawWireDisc(position, Vector3.forward, 0.4f); waypointToChange.pos = position; } Event e = Event.current; switch (e.GetTypeForControl(controlID))//this gets the event { case EventType.MouseMove: if (choosingPosition) position = GetWorldCoordinate(e.mousePosition); break; case EventType.MouseDown: if (e.button == 0)//left mouse button { GUIUtility.hotControl = controlID;//when we mouse down we make us the "hotControl" meaning the current one in control if (choosingPosition) { choosingPosition = false; waypointToChange = null; EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); } else { waypointToChange = GetClickedWaypoint(e.mousePosition);//will return null if one isn't clicked if (waypointToChange != null) choosingPosition = true; } e.Use(); } break; case EventType.MouseUp: if (e.button == 0)//left mouse button { GUIUtility.hotControl = 0;//when we mouse up we give control away e.Use(); } break; case EventType.KeyDown://not effected by making sure e.button == 0 if (e.keyCode == KeyCode.N) addWaypoint(); break; } EditorUtility.SetDirty(mr); serializedObject.ApplyModifiedProperties(); }
The following is an editted version of the default Text Shader for Unity. I only included the parts I changed for the sake of clear presentation. I also needed this clarity myself, so I had them marked in the original code using "//////".
Shader "Custom/TextFade" { Properties { ////////////// _DisolveSoeedBase("Disolve Speed Base", Float) = 540 _DisolveTexture("Disolve Texture", 2D) = "white" {} _DisolveTextureSize("Disolve Texture Size", Float) = 100 _DisolveXStart("Disolve X Start", Float) = 0 _DisolveYStart("Disolve Y Start", Float) = 0 _DisolveEdgeColor("Disolve Edge Color", Color) = (1, 1, 1, 1) _DisolveEdgeWidth("Disolve Edge Width", Float) = 50 _DisolveSpeedMultiplier("Disolve Speed Multiplier", Float) = 1 _DisolveStartTime("Disolve Line Start Time", Float) = 0 _DisolveY("Disolve Y", Float) = 0 _DisolveLineHeight("Disolve Line Height", Float) = 0 ////////////// } ////////////// float _DisolveSpeedMultiplier; float _DisolveSoeedBase; float _DisolveStartTime; float _DisolveTextureSize; sampler2D _DisolveTexture; float _DisolveXStart; float _DisolveYStart; float _DisolveY; float _DisolveLineHeight; half4 _DisolveEdgeColor; float _DisolveEdgeWidth; ////////////// fixed4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; ////////////// float xPos = ((_Time.y - _DisolveStartTime) * _DisolveSoeedBase / _DisolveSpeedMultiplier); float xResult = step(_DisolveXStart + xPos + tex2D(_DisolveTexture, IN.texcoord).r * _DisolveTextureSize, IN.worldPosition.x);//returns 1 if x < xPos float yResult = step(_DisolveYStart + _DisolveY, IN.worldPosition.y);//returns 1 if y < yPos float yPlusHeight = step(_DisolveY + _DisolveYStart - _DisolveLineHeight, IN.worldPosition.y);//returns 1 if y < yPos + lineHeight float xAndYPlusHeight = (1 - xResult) * yPlusHeight;//both have to be 1 for this to be 1 clip(yResult + xAndYPlusHeight - 1);//returns 1 or greater if xAndYPlusHeight OR yResult are 1 (plus adding in the texture for a little bit of dissonance) //has an offset from where it clips out float xResultColor = step(_DisolveXStart + xPos - _DisolveEdgeWidth + tex2D(_DisolveTexture, IN.texcoord).r * _DisolveTextureSize, IN.worldPosition.x); float yAreaOfCurrentLine = step(IN.worldPosition.y, _DisolveY + _DisolveYStart); float xAndYPlusHeightColor = xResultColor * yAreaOfCurrentLine;//both have to be 1 for this to be 1 color = color + half4(xAndYPlusHeightColor * _DisolveEdgeColor.r, xAndYPlusHeightColor * _DisolveEdgeColor.g, xAndYPlusHeightColor * _DisolveEdgeColor.b, 0); ////////////// return color; } }