Rocket League-clone

Rocket League-clone

This game was a little challenge for myself over a weekend. I wanted to create a simple sports game with physics, I created an AI opponent to have a quick way of testing the game. To keep it fair, the AI and Player use the same movement-code. It ended up surprisingly addictive and the AI can be tough to beat without exploiting its weaknesses.

usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;publicclassMatchPawn:MonoBehaviour{protectedTransformtf;protectedRigidbodyrb; [SerializeField]protectedTransformgoalToScoreIn; [Header("Movement")] [SerializeField]protectedfloatmovementSpeed=8.0f; [SerializeField]protectedfloatrotationSpeed=16.0f;protectedfloatspeedModifier=0.6f; [Header("Dash")] [SerializeField]protectedfloatdashForce=800f; [SerializeField]protectedfloatdashTime=0.2f; [SerializeField]protectedfloatdashCooldown=0.5f; [Header("Jump")] [SerializeField]protectedbooljumpEnabled=true; [SerializeField]protectedLayerMaskgroundLayer; [SerializeField]protectedfloatjumpForce=200f; [SerializeField]protectedfloatjumpTime=0.2f; [SerializeField]protectedfloatjumpCooldown=0.5f;protectedboolcanDash=true;publicboolisDashing{get;privateset;}protectedboolcanJump=true;protectedboolisJumping=false;protectedvirtualvoidAwake(){tf=transform;rb=GetComponent<Rigidbody>();isDashing=false;}protectedvirtualvoidGravity(){// If in the air, apply downward forceif(!Groundcheck())rb.AddForce(Vector3.down*80f,ForceMode.Acceleration);}#region JumpprotectedvirtualvoidStartJump(){canJump=false;isJumping=true;StartCoroutine(Jump());}// Adds an upward force to the playerprotectedvirtualIEnumeratorJump(){rb.AddForce(Vector3.up*jumpForce);// Length (time) of jumpyieldreturnnewWaitForSeconds(jumpTime);isJumping=false;// Cooldown before can jump againyieldreturnnewWaitForSeconds(jumpCooldown);canJump=true;}#endregion#region DashprotectedvirtualvoidStartDash(){canDash=false;isDashing=true;StartCoroutine(Dash());}// Adds a forward force to the playerprotectedvirtualIEnumeratorDash(){rb.AddForce(tf.forward*dashForce);// Length (time) of DashyieldreturnnewWaitForSeconds(dashTime);isDashing=false;// Cooldown before can dash againyieldreturnnewWaitForSeconds(dashCooldown);canDash=true;}#endregionprotectedvirtualboolGroundcheck(){// Cast a ray straight down, looking for the Ground-layerreturnPhysics.Raycast(tf.position,Vector3.down,0.7f,groundLayer);}// Reset position when a goal is scoredpublicvoidResetPosition(Vector3startPosition,floatdelay){StartCoroutine(MoveToStartPosition(startPosition,delay));}protectedvirtualIEnumeratorMoveToStartPosition(Vector3startPosition,floatdelay){rb.velocity=Vector3.zero;// Turn of physics for this Rigidbody when moving it without physicsrb.isKinematic=true;tf.position=startPosition;yieldreturnnewWaitForSeconds(delay);// Enable physics againrb.isKinematic=false;}protectedvirtualboolCanMove(){// When the game is resetting, make sure no movement can be executedreturn!GameManager.isResetting;}}

usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.AI;publicclassBasicAI:MatchPawn{ [Header("AI")] [SerializeField]privateTransformtarget; [SerializeField]privateTransformgoalToDefend;// Target = BallprivateVector3targetPosition;protectedoverridevoidAwake(){base.Awake();StartCoroutine(SetTargetPosition());}privatevoidUpdate(){LookRotation();}privatevoidFixedUpdate(){if(!CanMove())return;// Check if should Jumpif(Groundcheck()&&DistanceCheckOnXZ(2f)&&BallHeigthDifference()>2f&&canJump&&!isJumping)StartJump();// Check if should Dash to get closer to Ballif(DistanceGreaterThanCheckOnXYZ(8f)&&!InLineWithOwnGoal()&&canDash&&!isDashing)StartDash();// Check if should Dash to shoot the Ballelseif(DistanceCheckOnXYZ(3f)&&InLineWithBall()&&!InLineWithOwnGoal()&&canDash&&!isDashing)StartDash();Gravity();Move();}#region AI ChecksprivatefloatBallHeigthDifference(){returntarget.position.y-tf.position.y;}// Check angle to ball to decide if should dash for speed or to shootprivateboolInLineWithBall(){floatangle=Mathf.Abs(Vector3.Angle(target.position-tf.position,tf.forward));returnangle<15f;}// If the angle between ball and own goal is too small, don't shootprivateboolInLineWithOwnGoal(){Vector3ownGoal=newVector3(goalToDefend.position.x,tf.position.y,goalToDefend.position.z);floatangle=Mathf.Abs(Vector3.Angle(ownGoal-tf.position,tf.forward));returnangle<40f;}// Look towards opponents goal when lining up a shot to get the right directionprivateboolInLineWithOtherGoal(){Vector3otherGoal=newVector3(goalToScoreIn.position.x,tf.position.y,goalToScoreIn.position.z);floatangle=Mathf.Abs(Vector3.Angle(otherGoal-tf.position,tf.forward));returnangle<20f;}// Check the distance between Ball and AI on X & Z-axisprivateboolDistanceCheckOnXZ(floatminDistance){Vector3mPos=newVector3(tf.position.x,0f,tf.position.z);Vector3tPos=newVector3(target.position.x,0f,target.position.z);floatdistance=Vector3.Distance(mPos,tPos);returndistance<minDistance;}// Check the distance between Ball and AI on X, Y & Z-axisprivateboolDistanceCheckOnXYZ(floatminDistance){floatdistance=Vector3.Distance(tf.position,target.position);returndistance<minDistance;}// Check if the distance is too big, to dash for speedprivateboolDistanceGreaterThanCheckOnXYZ(floatmaxDistance){floatdistance=Vector3.Distance(tf.position,target.position);returndistance>maxDistance;}#endregionprivateIEnumeratorSetTargetPosition(){while(true){UpdateTargetPosition();yieldreturnnewWaitForSeconds(1f*Time.deltaTime);}}// Set the target position to be behind the ball (in line with opponents goal)privatevoidUpdateTargetPosition(){targetPosition=target.position+(target.position-goalToScoreIn.position).normalized;targetPosition.y=0.5f;}privatevoidLookRotation(){// Look at goal before shootingif(InLineWithOtherGoal()&&isDashing)tf.LookAt(goalToScoreIn);// Else just look at the Ball at all timeselsetf.LookAt(target);// Only apply rotation around Y (Up)tf.eulerAngles=Vector3.up*tf.eulerAngles.y;}privatevoidMove(){// Lower movement speed while jumpingif(!Groundcheck())speedModifier=0.8f;elsespeedModifier=1f;if(!isDashing){// Normal movement towards the ballVector3direction=(targetPosition-tf.position).normalized;direction.y=0f;rb.AddForce(direction*(movementSpeed*speedModifier)*Time.deltaTime);}else{// When not moving normally, clamp the velocity to the dash forcerb.velocity=Vector3.ClampMagnitude(rb.velocity,dashForce);}}protectedoverridevoidStartDash(){base.StartDash();}protectedoverrideIEnumeratorDash(){returnbase.Dash();}}

Dark Souls-clone

Dark Souls-clone

This game was the result of a 4-week course covering scripting, UI and UX. The assignment was to make a side-scroller with some basic mechanics and UI. I ended up making a game heavily inspired by Dark Souls, with mechanics such as: Bonfires, Inventory, collecting Souls and a Stamina-bar. I also implemented an in-game store where the Player can purchase buffs. The game features 2 levels, 2 different enemies and a boss.

Folder Creation Tool

Folder Creation Tool

This system was an experiment to learn Unity Editor-scripting. I got tired of creating the same folders each time I made a new project. It adds all the common folders with one click. To challange myself further I added functionality for custom folders and sub-folders. I learned a lot and still use it with every new project.

usingUnityEngine;usingUnityEditor;usingSystem.Collections;usingSystem.Collections.Generic;publicclassCreateFoldersWindow:EditorWindow{privateVector2scrollPos;privateColordefaultColor;// Info Message BoxprivatestringmessageBoxInfo="If a folder already exists, it will be ignored.";// Template presetsprivatebool[]projectTypes=newbool[2];privateintprojectTypeInt=0;privatestring[]projectTypeNames=newstring[]{"3D","2D"};// Template foldersprivateList<string>templateFolderNames;privateintnumberOfTemplateFolders;privateintstartTemplateFoldersAmount=6;// Custom foldersprivateList<string>customFolderNames;privateintnumberOfFolders;privateintnewNumberOfFolders;// Main Toolbar tabsprivatestaticinttoolbarInt;privatestring[]toolbarTabNames=newstring[]{"Custom","Standard"};// SettingsprivateboolcustomFolderDestination=false;privatestringfolderDestination="";// OPENS AND INITIALIZES [MenuItem("Window/Create Folders Tool %#q")]// KEY SHORTCUT: CTRL+SHIFT+QprivatestaticvoidOpenCreateFolders(){// CREATE THE WINDOWCreateFoldersWindowwindow=(CreateFoldersWindow)EditorWindow.GetWindow(typeof(CreateFoldersWindow),false,"Create Folders");// Load the icon for the windowTextureicon=AssetDatabase.LoadAssetAtPath<Texture>("Assets/Editor/Create Folders/Icon.png");GUIContenttc=newGUIContent("Folders",icon);window.titleContent=tc;AssetDatabase.Refresh();toolbarInt=1;window.defaultColor=GUI.color;// Initialize listswindow.InitializeCustomFoldersList();window.InitializeDefaultFoldersList();}// CLOSE THE WINDOWprivatevoidCloseCreateFolders(){// Reset listscustomFolderNames.Clear();numberOfFolders=0;// Close the windowEditorWindow.GetWindow<CreateFoldersWindow>().Close();}/* INITIALIZE THE CUSTOM LIST * SETTING VALUES TO DEFAULT AND MAKE NEW LIST */privatevoidInitializeCustomFoldersList(){numberOfFolders=0;newNumberOfFolders=numberOfFolders;customFolderNames=newList<string>();}// RESET THE CUSTOM LISTprivatevoidClearCustomList(){numberOfFolders=0;newNumberOfFolders=numberOfFolders;customFolderNames.Clear();customFolderNames=newList<string>();}// INITIALIZE DEFAULT NAMESprivatevoidInitializeDefaultFoldersList(){if(templateFolderNames==null){numberOfTemplateFolders=startTemplateFoldersAmount;templateFolderNames=newList<string>();}elseif(templateFolderNames!=null){templateFolderNames.Clear();numberOfTemplateFolders=startTemplateFoldersAmount;}for(inti=0;i<projectTypes.Length;i++){projectTypes[i]=true;}DefaultNameTemplates(projectTypeInt);}/* COMMON FOLDER NAMES TEMPLATE * -CAN BE ADDED TO- */privatevoidDefaultNameTemplates(inttype){if(type==0){templateFolderNames.Insert(0,"Animation");templateFolderNames.Insert(1,"Audio");templateFolderNames.Insert(2,"Materials");templateFolderNames.Insert(3,"Models");templateFolderNames.Insert(4,"Prefabs");templateFolderNames.Insert(5,"Scenes");templateFolderNames.Insert(6,"Scripts");}elseif(type==1){templateFolderNames.Insert(0,"Animation");templateFolderNames.Insert(1,"Audio");templateFolderNames.Insert(2,"Materials");templateFolderNames.Insert(3,"Prefabs");templateFolderNames.Insert(4,"Scenes");templateFolderNames.Insert(5,"Scripts");templateFolderNames.Insert(6,"Sprites");}}// GROW TEMPLATE LISTprivatevoidIncreaseTemplateFolderNamesList(){if(numberOfTemplateFolders<10){numberOfTemplateFolders++;// INSERT EMPTY ROW TO WRITE NEW NAME ONtemplateFolderNames.Insert(numberOfTemplateFolders,"");}elsereturn;}// CLEAR EMPTY ROWS IN TEMPLATE LISTprivatevoidClearEmptyRows(){for(inti=6;i<templateFolderNames.Count;i++){if(templateFolderNames[i]==""){templateFolderNames.RemoveAt(i);numberOfTemplateFolders--;}}}/* UPDATE NUMBER OF ROWS FOR CUSTOM NAMES * INSERTS NEW IF NUMBER IS HIGHER * REMOVES IF LOWER */privatevoidUpdateCustomFolderNamesList(intindex){// INSERTfor(inti=newNumberOfFolders;i<index;i++)customFolderNames.Add("");// REMOVEif(newNumberOfFolders>numberOfFolders)customFolderNames.RemoveAt(index);newNumberOfFolders=index;}privatevoidOnGUI(){scrollPos=EditorGUILayout.BeginScrollView(scrollPos);EditorGUILayout.BeginVertical(GUILayout.Width(40f));GUILayout.Space(10f);// MODE SELECTION TABStoolbarInt=GUILayout.Toolbar(toolbarInt,toolbarTabNames);switch(toolbarInt){case0:// CUSTOM FOLDER NAMESCustomFoldersView();break;case1:// TEMPLATE FOLDER NAMESDefaultFoldersView();break;}GUI.color=defaultColor;EditorGUILayout.HelpBox(messageBoxInfo,MessageType.Info);EditorGUILayout.EndVertical();EditorGUILayout.EndScrollView();}privatevoidCustomFoldersView(){GUILayout.Space(10f);// ADVANCED SETTINGS (Sub folders)customFolderDestination=EditorGUILayout.BeginToggleGroup("Custom path",customFolderDestination);if(customFolderDestination){GUILayout.Box("Folder destination:");EditorGUILayout.HelpBox("Make sure path exists!",MessageType.Warning);EditorGUILayout.BeginHorizontal();GUILayout.Label("Assets/");folderDestination=EditorGUILayout.TextField(folderDestination);EditorGUILayout.EndHorizontal();}elseif(!customFolderDestination)folderDestination="";EditorGUILayout.EndToggleGroup();ClearEmptyRows();GUILayout.Space(10f);GUILayout.Label("How many folders:");EditorGUILayout.BeginHorizontal();numberOfFolders=EditorGUILayout.IntField(Mathf.Clamp(numberOfFolders,0,10));if(GUILayout.Button("Reset",GUILayout.Width(100f),GUILayout.Height(20f))){if(numberOfFolders>0){ClearCustomList();}return;}GUILayout.Space(10f);EditorGUILayout.EndHorizontal();// Create rows based on how many folders we want(numberOfFolders)if(numberOfFolders>0){GUILayout.Space(10f);GUILayout.Label("Folder names:");EditorGUILayout.BeginVertical();// Create row with text field based on sliderfor(inti=0;i<numberOfFolders;i++){UpdateCustomFolderNamesList(numberOfFolders);// Input name of foldercustomFolderNames[i]=EditorGUILayout.TextField(customFolderNames[i]);}EditorGUILayout.EndVertical();}GUILayout.Space(20f);GUI.color=Color.cyan;// CREATE FOLDERS BUTTONif(GUILayout.Button("Create Folders",GUILayout.Width(200f),GUILayout.Height(50f))){CreateFoldersFromList(customFolderNames);}}privatevoidDefaultFoldersView(){GUILayout.Space(10f);EditorGUILayout.BeginVertical(GUILayout.Width(25f));// CHOOSE PRESET (3D or 2D)projectTypeInt=GUILayout.Toolbar(projectTypeInt,projectTypeNames);switch(projectTypeInt){case0:// 3Dif(projectTypes[0]){// Clear list and get new template based on typeInitializeDefaultFoldersList();/* Make it only update when switched * other type is always true before switch */projectTypes[0]=false;projectTypes[1]=true;}break;case1:// 2Dif(projectTypes[1]){// Clear list and get new template based on typeInitializeDefaultFoldersList();/* Make it only update when switched * other type is always true before switch */projectTypes[1]=false;projectTypes[0]=true;}break;}EditorGUILayout.EndVertical();EditorGUILayout.BeginHorizontal();// Add an empty row to write inif(GUILayout.Button("Add Folder",GUILayout.Width(100f),GUILayout.Height(20f))){IncreaseTemplateFolderNamesList();}// Reset names to default namesif(GUILayout.Button("Reset",GUILayout.Width(100f),GUILayout.Height(20f))){InitializeDefaultFoldersList();return;}EditorGUILayout.EndHorizontal();GUILayout.Space(10f);GUILayout.Label("Folder names:");// Set template names and name for added empty rowsfor(inti=0;i<=numberOfTemplateFolders;i++){templateFolderNames[i]=EditorGUILayout.TextField(templateFolderNames[i]);}GUILayout.Space(20f);GUI.color=Color.cyan;// CREATE FOLDERS BUTTONif(GUILayout.Button("Create Folders",GUILayout.Width(200f),GUILayout.Height(50f))){CreateFoldersFromList(templateFolderNames);}}// CREATE THE FOLDERS (when 'Create Folders' button is pressed)privatevoidCreateFoldersFromList(List<string>folderNames){// Check if we have a custom destination for foldersif(!customFolderDestination)folderDestination="Assets";else{folderDestination="Assets/"+folderDestination;}boolcreatedFolders=false;intfolderCount=0;// If folders dont already exist, create themfor(inti=0;i<folderNames.Count;i++){if(!AssetDatabase.IsValidFolder(folderDestination+"/"+folderNames[i])&&folderNames[i]!=""){AssetDatabase.CreateFolder(folderDestination,folderNames[i]);folderCount++;createdFolders=true;}}SetMessageBoxInfo(createdFolders,folderCount);DoneCreating();}/* SHOW CONSOLE MESSAGE * WHEN DONE CREATING NEW FOLDERS * REFRESH DATABASE THEN RESET ALL VALUES */privatevoidDoneCreating(){AssetDatabase.Refresh();DefaultNameTemplates(0);ClearCustomList();}privatevoidSetMessageBoxInfo(boolsuccess,intamount){if(success){messageBoxInfo="Folders created: "+amount;}else{messageBoxInfo="Folder already exists!";}}}

Blood System

Blood System

I wanted to make an FPS-game from scratch with lots of blood and ended up with this system. It works by changing the pixels on a material. The blood-particle system raycasts from its particles and checks if the surface hit can be painted.

usingUnityEngine;usingSystem.Collections;publicclassHitParticle:MonoBehaviour{privateParticleSystemps;privateParticleSystem.Particle[]particles; [SerializeField]privateLayerMaskcollisionMask;privateboolinUse=false;privatefloattimer;// Lower value to optimizeprivateintmaxParticlesToCheck=7;privatevoidAwake(){ps=GetComponent<ParticleSystem>();// Populate the particles-arrayparticles=newParticleSystem.Particle[ps.maxParticles];}privatevoidLateUpdate(){DetectCollisions();// Start the timer when this particle system is in useif(inUse){timer-=Time.deltaTime;// When the particle systems lifetime is over, re-parent to the object poolif(timer<=0){transform.SetParent(HitParticlesPooler.Instance.transform);inUse=false;}}}privatevoidDetectCollisions(){intparticlesAlive=ps.GetParticles(particles);// Update the amount of particles currently active in this systemps.SetParticles(particles,particlesAlive);for(inti=0;i<particlesAlive;i++){if(i<maxParticlesToCheck&&particlesAlive>0){Vector3dir=particles[i].position-transform.position;RaycastHithit;// Raycast from the current particle with its direction from particle emitterif(Physics.Raycast(particles[i].position,dir,outhit,3f,collisionMask)){if(hit.collider!=null){// If a paintable surface is hitif(hit.collider.GetComponent<SurfacePainter>()){hit.collider.GetComponent<SurfacePainter>().PaintBlood(hit.textureCoord,hit.normal,ps.startColor);}}}}else{// Break out of the loop if the amount to check is done or if no particles are alivebreak;}}}publicvoidUseParticle(){ps.Play();timer=ps.startLifetime;// Start the LateUpdate timerinUse=true;}}

usingUnityEngine;usingSystem.Collections;usingSystem.Collections.Generic;publicclassSurfacePainter:MonoBehaviour{privateenumSurfaceType{Geometry,LivingEntity}; [SerializeField]privateSurfaceTypesurfaceType;privateRendererrend;privateTexture2DoriginalTex;// The texture instance to manipulateprivateTexture2Dtex; [SerializeField]privateColorbulletHoleColor; [SerializeField]privateColor[]shotBloodColor;// The color to apply to the current pixelprivateColorpixelColor;privatevoidStart(){rend=GetComponent<Renderer>();originalTex=rend.material.mainTextureasTexture2D;CreateNewTexture();SetPixelColor();}// Creates a texture instance based on the original texture and assigns it to the material rendererprivatevoidCreateNewTexture(){tex=newTexture2D(originalTex.width,originalTex.height);tex.filterMode=FilterMode.Point;tex.SetPixels(originalTex.GetPixels());tex.Apply();rend.material.mainTexture=tex;}// Decide if it's a bullet hole or bloodprivatevoidSetPixelColor(){switch(IsLivingEntity()){casefalse:pixelColor=bulletHoleColor;break;casetrue:pixelColor=shotBloodColor[0];break;}}privateboolIsLivingEntity(){if(surfaceType==SurfaceType.LivingEntity)returntrue;returnfalse;}publicvoidPaintBlood(Vector2hitPoint,Vector3hitNormal,ColorparticleColor){// Get the UV coordinates of the hitintuvX=(int)(hitPoint.x*tex.width);intuvY=(int)(hitPoint.y*tex.height);Vector2pixelUV=newVector2(uvX,uvY);// Loop through the pixels of the texturefor(inty=0;y<tex.height;y++){for(intx=0;x<tex.width;x++){// Update the correct pixel with its new colorif(x==uvX&&y==uvY){tex.SetPixel(uvX,uvY,particleColor);tex.Apply();// Start the blood drip from the woundStartCoroutine(BloodDrip(pixelUV,hitNormal,particleColor,false));}}}}publicvoidPaintSurface(Vector2hitPoint,Vector3hitNormal){// Get the UV coordinates of the hitintuvX=(int)(hitPoint.x*tex.width);intuvY=(int)(hitPoint.y*tex.height);Vector2pixelUV=newVector2(uvX,uvY);// Loop through the pixels of the texturefor(inty=0;y<tex.height;y++){for(intx=0;x<tex.width;x++){// Update the correct pixel with its new colorif(x==uvX&&y==uvY){tex.SetPixel(uvX,uvY,pixelColor);tex.Apply();}}}}privateIEnumeratorBloodDrip(Vector2pos,Vector3hitNormal,Colorcol){// Make the length of the drip a bit randomintdripAmount=Random.Range(2,5);for(inty=1;y<dripAmount;y++){// If the hit normal is pointing up, spread it randomly around the hit positionif(hitNormal.y>=1){// Store the hit position so it can start from the same position the next loopinttempX=(int)pos.x;inttempY=(int)pos.y;// Drip either left or rightintxDir=Random.Range(0,2);if(xDir==0)xDir=-1;pos.x+=xDir;// Drip either forward or backintyDir=Random.Range(0,2);if(yDir==0)yDir=-1;pos.y+=yDir;tex.SetPixel((int)pos.x,(int)pos.y,col);// Reset the start position to the hit positionpos.x=tempX;pos.y=tempY;}else{// Normal hit, drip downtex.SetPixel((int)pos.x,(int)pos.y-y,col);}tex.Apply();// Small delay between dripsyieldreturnnewWaitForSeconds(0.2f);}}}

FPS Project

FPS Project

A small project made in 2 weeks with Unreal Engine. In this project I focused on making an interesting level with Scripted Events and AI encounters leading up to a boss fight. All scripting was done using Unreal Blueprints.