The Game

The arenas of the Roman Empire have all seen better days. Thankfully, the newly discovered gunpowder is here to bring the boredom to an end! Equipped with the all new arm cannon, the gladiators slash, bash and blast their foes into smithereens in an arena filled with deadly traps and hazards.

Guns Guts & Glory is a local multiplayer brawler that pits up to 4 friends against each other in chaotic and intense battles on a dynamically changing arena.

My role

Combat Gameplay

Camera-system

Player Input / Interaction

Player Setup

Art / Effects / Animation-implementation

Level Events

My main responsibilities were the combat gameplay and camera system. I worked a lot with player and weapon-interactions such as taking damage, picking up weapons/ammo and attacking. I also created an easy way to assign different armor and colors to each player at runtime.

Other responsibilities include: Level scripting, such as destruction, traps and weapon spawning.

Weapons & Damage

The game features 4 different melee-weapons. They all use the same script but behave different depending on the weapon type selected and can be tweaked further with the variables visible in the inspector.

Health

We wanted the violence to be over the top and cartoony with blood effects that would match the type of weapon used.

I created 3 different methods to handle the type of damage a player can receive. Weapons have their own damage type such as sharp and blunt and provide that info upon colliding with a player. From there the Health-script makes sure that the correct effect/damage/knockback etc is used.

Melee weapons

All the weapons in-game can be picked up and dropped at any time. I never destroy or spawn them when used/dropped. The interactions use different collision types. I switch the colliders on and off to ensure that only the right collision event is used.

All the properties such as damage and impact force can be set in the inspector to allow fast testing and tweaking. This approach really helped us improve the balance of weapons when we did playtests.

privatevoidCheckCollision(Colliderother){if(ownerCombatController==null)return;Vector3hitDirection=(other.transform.position-ownerCombatController.transform.position).normalized;// We dont care about the Y in the directionhitDirection.y=0;#region Mace ground hitif(ownerCombatController.isPrimaryAttacking){if(!other.CompareTag("Player")){if(weaponType==WeaponType.Mace){floatradius=4f;Collider[]players=Physics.OverlapSphere(impactPoint.position,radius,objectsAffectedByImpact);for(inti=0;i<players.Length;i++){Vector3direction=(players[i].transform.position-ownerCombatController.transform.position).normalized;direction.y=0f;if(players[i]!=ignoreCollider){Playerplayer=players[i].GetComponent<Player>();player.Knockback(direction,impactForce);player.SetLastDamageDealer(ownerCombatController);}}}SpawnGeometryHitEffect(false,impactPoint.position);return;}}#endregion// Check if the object hit can be damagedIDamagabledamagable=other.GetComponent<IDamagable>();#region Other = IDamagableif(damagable!=null&&damagable.CanTakeDamage()){// If this is the final hit before the other Player diesif(damagable.IsKillingBlow(GetCurrentDamage())&&!damagable.IsDead()){// Damage the player and attach this weapondamagable.TakeWeaponDamage(ownerCombatController,GetCurrentDamage(),hitDirection,impactForce,weaponType,weaponDamageType);// STATSownerCombatController.IncreaseDamageDealtStats(GetCurrentDamage());canDamage=false;}// or just a normal hitelse{if(!damagable.IsDead()){damagable.TakeWeaponDamage(ownerCombatController,GetCurrentDamage(),hitDirection,impactForce,weaponType,weaponDamageType);ownerCombatController.IncreaseDamageDealtStats(GetCurrentDamage());canDamage=false;}}}#endregion#region Reset Damage coroutineif(resetCanDamage!=null){StopCoroutine(resetCanDamage);resetCanDamage=null;}resetCanDamage=ResetCanDamage();StartCoroutine(resetCanDamage);#endregion}

privatevoidWorldCollision(Collisioncollision){// Get the position of the first collisionContactPointcontactPoint=collision.contacts[0];SpawnGeometryHitEffect(contactPoint.point);// Reset throwAttack when weapon collides with the level ResetThrowAttack();// Disable spear trail effectif(weaponType==WeaponType.Spear&&weaponTrail.enabled)weaponTrail.enabled=false;}

publicvoidTakeWeaponDamage(CombatControllerdamageDealer,intdamage,Vector3hitDirection,floatforceToAddToPlayer,WeaponTypeweaponType,WeaponDamageTypedamageType){// Who dealt the damageHandleLastDamageDealer(damageDealer);if(forceToAddToPlayer>0)player.Knockback(hitDirection,forceToAddToPlayer);LoseHealth(damage,hitDirection,damageType);SpawnWeaponHitEffect(hitDirection,weaponType);OnTakeDamageAudio();}publicvoidTakeProjectileDamage(CombatControllerdamageDealer,intdamage,Vector3hitDirection,floatforceToAddToPlayer,WeaponDamageTypedamageType){// Who dealt the damageHandleLastDamageDealer(damageDealer);if(forceToAddToPlayer>0)player.Knockback(hitDirection,forceToAddToPlayer);LoseHealth(damage,hitDirection,damageType);SpawnTrapHitEffect();OnTakeDamageAudio();}publicvoidTakeOtherDamage(intdamage,Vector3hitDirection,floatforceToAddToPlayer,WeaponDamageTypedamageType){if(forceToAddToPlayer>0)player.Knockback(hitDirection,forceToAddToPlayer);LoseHealth(damage,hitDirection,damageType);// Spawn blood effect if the Damage type is not Waterif(damageType!=WeaponDamageType.Water)SpawnTrapHitEffect();OnTakeDamageAudio();}

Arm Cannon

Ammo must be picked up from the level and is thrown in to the arena by the crowd whenever a player gets a kill.

There are two type of projectiles: an explosive cannonball and a net. The cannonball is a guaranteed kill on hit but also explodes and can damage additional players depending on their distance to it. The net slows players to a crawl which allows you to stop them right on top of a deadly trap or to get in close with a melee weapon.

publicvoidShoot(CombatControllerownerCombatController){if(!CanShoot())return;// Instantiate the projectile with the rotation of the PlayerGameObjectprojectile=Instantiate(currentProjectile,projectileSpawnPoint.position,Quaternion.LookRotation(ownerCombatController.transform.forward,Vector3.up));Vector3euler=projectile.transform.eulerAngles;// Zero out the X-axis (Up) to avoid shooting in to the ground or above other Playerseuler.x=0f;projectile.transform.eulerAngles=euler;projectile.GetComponent<Projectile>().Shoot(ownerCombatController);// Small delay before the shoot effect, to make it follow the arm cannon betterStartCoroutine(SpawnShootEffect());gunAudio.clip=gunSounds[Random.Range(0,gunSounds.Count)];gunAudio.Play();RemoveAmmo();}

privatevoidDoDamage(Transformplayer){IDamagabledamagable=player.GetComponent<IDamagable>();// Get direction between player and this projectileVector3direction=(player.position-transform.position).normalized;if(damagable!=null&&damagable.CanTakeDamage()){// Check if the distance is within lethal range and apply max damageif(Vector3.Distance(transform.position,player.transform.position)<(explosionRadius*0.5f)){// Check if this damage will kill the Playerif(damagable.IsKillingBlow(maxDamage)){ownerCombatController.IncreaseDamageDealtStats(maxDamage);}damagable.TakeProjectileDamage(ownerCombatController,maxDamage,direction,impactForce,weaponDamageType);}// Otherwise, normal (non-lethal) damageelse{// Check if this damage will kill the Playerif(damagable.IsKillingBlow(damage)){ownerCombatController.IncreaseDamageDealtStats(damage);}damagable.TakeProjectileDamage(ownerCombatController,damage,direction,impactForce,weaponDamageType);}}}

The Camera

Intermission & Round Start

Between each round there is a small break were the players get to choose a spawn position and see the current score displayed with UI and a 2D representation of the level. During this time the camera positions itself with a nice overview of the whole arena and changes the depth of field to achieve a unfocused effect.

When the countdown before the round starts, the camera starts to move down closer to the players and gradually change the depth of field back to normal. This was accomplished by using Animation Curves and using the value of the curve in a lerp of the position and rotation.

Movement & Zoom

This was my first time making a system that had to work dynamically based on how many players it currently needs to track. I ended up using a list to store all coordinates of the active players and sorting the min and max-values. Then basing the camera position in the middle of these values.

The zoom was handled by parenting the camera object to an empty object acting as the rig, which was tilted to the desired angle. The camera then moved along its local Forward-axis to zoom in and out based on the distance between the two players furthest away from each other.

// Find the min and max position of Players in the levelprivatevoidGetMinMaxPositions(){// Add all player positionfor(inti=0;i<players.Count;i++){xPositions.Add(players[i].position.x);yPositions.Add(players[i].position.z);}// Find the min and max positions on X & YmaxX=Mathf.Max(xPositions.ToArray());maxY=Mathf.Max(yPositions.ToArray());minX=Mathf.Min(xPositions.ToArray());minY=Mathf.Min(yPositions.ToArray());// Make vectors for min and maxmin=newVector3(minX,0f,minY);max=newVector3(maxX,0f,maxY);}// Set the new position for the camrea to move towardsprivatevoidSetTargetPosition(){// Get middle between min and maxtargetPos=(min+max)*0.5f;// Make the target position vector (for the Camera rig transform)newRigPosition=newVector3(targetPos.x,2f,targetPos.z);}privatevoidUpdatePositionAndZoom(){if(currentMode!=CameraMode.Normal){return;}else{// Get distance between min and maxdistance=Vector3.Distance(min,max);SetSizeOfPlayerCanvasBasedOnDistanceFromCamera();targetZPos=Mathf.Clamp(distance,minZoom,maxZoom);// Get the direction from the target position to the cameranewCameraPosition=camTransform.TransformDirection(-tf.up*targetZPos);// Set the destination position of the cameranewCameraPosition=(Vector3.forward*newCameraPosition.z);}}privatevoidSetSizeOfPlayerCanvasBasedOnDistanceFromCamera(){for(inti=0;i<playerCanvas.Count;i++){if(playerCanvas[i]!=null){floatsize=Vector3.Distance(camTransform.position,playerCanvas[i].parent.position);playerCanvas[i].localScale=Vector3.one*(size*0.08f);}}}privatevoidMoveCameraAndRig(){// Lerp the camera z-position (Zoom) to the desired positioncamTransform.localPosition=Vector3.Lerp(camTransform.localPosition,newCameraPosition,lerpSpeedY*Time.deltaTime);// Lerp the Camera Rig to the desired positiontf.position=Vector3.Lerp(tf.position,newRigPosition,lerpSpeedXZ*Time.deltaTime);}

The Player

Armor & Color

We started out by working with one player prefab for each player that was set up with its unique armor and color etc. We realized quite fast that each time we needed to change something that applied to all of them such as the character model, it took forever to have to do that 4 times.

Instead I created a scriptable object class to store all relevant info in. Then I only needed to create 4 of those and set them up for use with a generic player prefab that all players share. When the players are spawned at the start of a match they are assigned the info relevant to the ID of their controller, which then sets up armor and color.

This ended up saving us a lot of time since we were constantly iterating and improving parts of the prefabs hierarchy.

// Set the ID and get the Player colorprotectedvoidInitPlayerInfo(){// Get the Player IDplayerID=playerInfo.PlayerID;// Enable the specific armor for the PlayerSetPlayerArmor();SetPlayerArmCannon();SetPlayerHelmet();SetPlayerColorOnMaterials();meshes=newGameObject[4];meshes[0]=bodyMesh;meshes[1]=hipsJNT;meshes[2]=pants;meshes[3]=capeConnector;}/// <summary>/// Gets the player info/// </summary>/// <returns>The player info</returns>publicPlayerInfoGetPlayerInfo(){returnplayerInfo;}// Activates the armor parts for this Player based on Player IDprotectedvoidSetPlayerArmor(){TransformplayerMesh=transform.GetChild(0);for(inti=0;i<4;i++){// Disable all the pants that do not match the Player IDif(i!=playerID){playerMesh.Find("SM_Pants"+(i+1)).gameObject.SetActive(false);}else{pants=playerMesh.Find("SM_Pants"+(i+1)).gameObject;}}}protectedvoidSetPlayerArmCannon(){TransformarmCannon=GetComponentInChildren<Gun>().transform;GameObject[]armCannonMeshes=newGameObject[2];intindex=0;for(inti=0;i<armCannon.childCount;i++){if(armCannon.GetChild(i).CompareTag("ArmCannon")){armCannonMeshes[index]=armCannon.GetChild(i).gameObject;index++;}}// Give Player 2 & 4 the second arm cannonif(PlayerID==0||PlayerID==2){if(armCannonMeshes[1].gameObject.activeInHierarchy)armCannonMeshes[1].SetActive(false);if(!armCannonMeshes[0].gameObject.activeInHierarchy)armCannonMeshes[0].SetActive(true);}else{if(armCannonMeshes[0].gameObject.activeInHierarchy)armCannonMeshes[0].SetActive(false);if(!armCannonMeshes[1].gameObject.activeInHierarchy)armCannonMeshes[1].SetActive(true);}}// Create a helmet for the PlayerprotectedvoidSetPlayerHelmet(){// Fix for helmet mesh position offsetplayerInfo.Helmet.transform.position=Vector3.zero;helmet=Instantiate(playerInfo.Helmet,helmetPosition,false);foreach(MeshRendereriteminhelmet.GetComponents<MeshRenderer>()){Material[]mats=item.materials;foreach(Materialmatinmats){if(mat.HasProperty("_PlayerColor")){mat.SetColor("_PlayerColor",playerInfo.PlayerColor);}}}}/// <summary>/// Call after setting Armor & Helmet /// </summary>protectedvoidSetPlayerColorOnMaterials(){stringcolorProperty="_PlayerColor";ColorplayerColor=playerInfo.PlayerColor;if(!inCharacterSelection){// Make new color to allow setting the alphaColorcol=playerColor;col.a=health.healthBarFill.color.a;// Set color of the health bar;health.InitHealthUI(colorProperty,col);health.HideLeaderFrame();// Set color of the CombatControllers "aimLine" (LineRenderer)combat.InitAimLineMaterials(colorProperty,playerColor);}// Set color of all armorforeach(SkinnedMeshRenderersmrinGetComponentsInChildren<SkinnedMeshRenderer>()){if(smr.name=="SK_hero")smr.material.SetTexture("_MainTex",playerInfo.SkinTexture);// Only set the color of materials that contain the "_PlayerColor" propertyif(smr.material.HasProperty(colorProperty)){if(smr.name=="SM_Cape_Connector1"){capeConnector=smr.gameObject;}smr.material.SetColor(colorProperty,playerColor);// Get the correct shape and number on the capeif(smr.material.HasProperty("_UVX"))smr.material.SetFloat("_UVX",playerInfo.CapeUV.x);if(smr.material.HasProperty("_UVY"))smr.material.SetFloat("_UVY",playerInfo.CapeUV.y);}}}

Input

Since the game is a fast and chaotic multiplayer game we wanted the controls to be easy to learn. At first we had one button each for attack, throw and shoot but we learned that it was too many buttons for our type of gameplay. I removed the need for a third button by having both throw and shoot on the same button. This also worked in favor of the gameplay we desired, the button is used to shoot whenever the arm cannon is loaded to prioritize the explosive side of combat and to throw otherwise.

Movement

The final version of the movement system came quite late thanks to the very late addition of a knockback-effect, which was a much needed addition to the combat gameplay. I had to re-write most of it to not allow the movement input to affect the velocity while being in the knockback-state.

// Normal melee attackif(InputManager.GetAttackInputDown(player.PlayerID)&&!isPrimaryAttacking&&!isSecondaryAttacking){StartPrimaryAttack();}else{if(canSecondaryAttack){if(armCannon.CanShoot()&&!isThrowing){// Arm cannon attackHandleShootInputDownAndHeld(InputManager.GetThrowInputDown(player.PlayerID),InputManager.GetThrowInput(player.PlayerID),outisShootInputHeld,outwasShootInputReleased,wasShootInputHeldLastFrame);// Only update wasShootInputHeldLastFrame if the game is not pausedif(!GameController.gamePaused&&!isPrimaryAttacking)wasShootInputHeldLastFrame=isShootInputHeld;// Must shoot when there is ammo loadedif(armCannon.CanShoot())return;}elseif(!isShooting&&hasWeapon){// Throw weapon attackHandleThrowInputDownAndHeld(InputManager.GetThrowInputDown(player.PlayerID),InputManager.GetThrowInput(player.PlayerID),outisThrowInputHeld,outwasThrowInputReleased,wasThrowInputHeldLastFrame);// Only update wasThrowInputHeldLastFrame if the game is not pausedif(!GameController.gamePaused&&!isPrimaryAttacking)wasThrowInputHeldLastFrame=isThrowInputHeld;}}}

// Checks for input for ThrowingprivatevoidHandleThrowInputDownAndHeld(boolinputDown,boolinputHeld,outboolisInputHeld,outboolwasInputReleased,boolwasInputReleasedLastFrame){// Start throw animationif(inputDown&&!isPrimaryAttacking&&!isSecondaryAttacking){isSecondaryAttacking=true;isThrowing=true;StartChargeThrow();// Set a different Movement speed based on what the Player is doingUpdateMovementModifier();// Enable the lineEnableAimLine(true,true);}// Set is throw heldisInputHeld=inputHeld;// Check if it was held last frame and not this one to determine of it was releasedwasInputReleased=wasInputReleasedLastFrame&&!isInputHeld;// Charge if the button is heldif(isInputHeld&&!isPrimaryAttacking&&isSecondaryAttacking){// Increase the modifier while the button is heldthrowForceModifier+=(weaponThrowChargeSpeedModifier*Time.deltaTime);currentThrowForce=((normalThrowForce*weaponThrowLengthModifier)*throwForceModifier);currentThrowForce=Mathf.Clamp(currentThrowForce,normalThrowForce*weaponThrowLengthModifier,maxThrowForce*weaponThrowLengthModifier);// Set the position of the lineSetAimLine(meleeWeaponHold,false);}// Only release the throw if the game is not pausedelseif(wasInputReleased&&!isInputHeld&&!isPrimaryAttacking&&isSecondaryAttacking&&!GameController.gamePaused){StartSecondaryAttack();// Set a different Movement speed based on what the Player is doingUpdateMovementModifier();// Disable the lineEnableAimLine(false,true);canSecondaryAttack=false;}}// Checks for input for shootingprivatevoidHandleShootInputDownAndHeld(boolinputDown,boolinputHeld,outboolisInputHeld,outboolwasInputReleased,boolwasInputReleasedLastFrame){// Start throw animationif(inputDown&&!isPrimaryAttacking&&!isSecondaryAttacking){isSecondaryAttacking=true;isShooting=true;StartRaiseArmCannon();// Set a different Movement speed based on what the Player is doingUpdateMovementModifier();// Enable the lineEnableAimLine(true,false);}// Set is throw heldisInputHeld=inputHeld;// Check if it was held last frame and not this one to determine of it was releasedwasInputReleased=wasInputReleasedLastFrame&&!isInputHeld;// Charge if the button is heldif(isInputHeld&&!isPrimaryAttacking&&isSecondaryAttacking){// Set the position of the line and set lengthif(armCannon.currentProjectile.GetComponent<Net>())SetAimLine(armCannon.projectileSpawnPoint,true,9f);// Set the position of the line getting default length (13f)elseSetAimLine(armCannon.projectileSpawnPoint,true);}// Only release the throw if the game is not pausedelseif(wasInputReleased&&!isInputHeld&&!isPrimaryAttacking&&isSecondaryAttacking&&!GameController.gamePaused){StartArmCannonShoot();// Set a different Movement speed based on what the Player is doingUpdateMovementModifier();// Disable the lineEnableAimLine(false,false);canSecondaryAttack=false;}}

Player Animations

I set up the animations for the Player. The player is controlled with “twin-stick” controls and needed to be able to change the walk animation depending on the facing direction in relation to the movement direction.

Everything except for the legs of the player needs to be able to move independent of the legs so I created a separate layer for the upper body where all of the attacking takes place. This layer overrides the other layer whenever the player does more than movement.

privatevoidMovementAnimation(Vector3moveInput){// Choose animation based on the direction the player is facingfloatdirX=Vector3.Dot(directionLeftStick,tf.right);floatdirY=Vector3.Dot(directionLeftStick,tf.forward);if(moveInput!=Vector3.zero)SetMovementBlendTree(dirX,dirY);elseSetMovementBlendTree(0f,0f);}publicvoidSetMovementBlendTree(floatvelocityX,floatvelocityY){// Blend smoother to prevent cape from going crazyvelocityX=Mathf.MoveTowards(anim.GetFloat("VelocityX"),velocityX,speed*Time.deltaTime);velocityY=Mathf.MoveTowards(anim.GetFloat("VelocityY"),velocityY,speed*Time.deltaTime);anim.SetFloat("VelocityX",Mathf.Clamp(velocityX,-1f,1f));anim.SetFloat("VelocityY",Mathf.Clamp(velocityY,-1f,1f));}

Level

Destruction

During the second out of 3 rounds of a match the outer ring of the arena will start exploding one piece at a time until the arena has shrunk by one ring at the end of the round. This makes the combat of the third round very close quarters which makes certain weapons and traps even deadlier.

2D Map

The spawn select-map contains information about where weapons and traps are located. I made a method for converting their world position to positions on the 2D canvas. This way we could spawn traps and weapons in any position and get the correct information without moving icons on the map with each change.

privateIEnumeratorDestruction(){while(true){booldone=false;TransformcurrentPart;if(!outerRingsDestroyed){currentPart=outerRings[currentPartIndex];// If currentPartIndex is equal to the length of the list we are done with the outer ringif(++currentPartIndex==outerRings.Count){currentPartIndex=0;outerRingsDestroyed=true;}}else{if(!innerRingsStarted)innerRingsStarted=true;currentPart=innerRings[currentPartIndex];if(++currentPartIndex==innerRings.Count){done=true;}}yieldreturnStartCoroutine(Shake(currentPart));if(done){EndLevelDestruction();yieldbreak;}}}privateIEnumeratorShake(TransformcurrentPart){floattimer=0f;// Spawn destruction effect#region Effect position, rotation and size// Instantiate the effect at the position of the current part and correct y-positionTransformeffect=Instantiate(destructionEffect,transform.position+(Vector3.up*2f),Quaternion.identity);// Find all the Particle systems in the instantiated effectParticleSystem[]ps=effect.GetComponentsInChildren<ParticleSystem>();// Default offset for effect rotationfloatoffset=30f;// Set the arc of all ParticleSystem to match the current partfor(inti=0;i<ps.Length;i++){ParticleSystem.ShapeModuleshape=ps[i].shape;if(!innerRingsStarted){if(currentPart.name.Contains("0.5")){if(ps[i].name=="PS_Rocks"){TransformrockEffect=ps[i].transform;rockEffect.gameObject.SetActive(false);}}}elseif(innerRingsStarted){if(ps[i].name=="PS_Rocks Copy"){TransformrockEffect=ps[i].transform;Vector3pos=(currentPart.position-rockEffect.localPosition).normalized;ps[i].transform.localPosition+=pos*4f;}elseif(ps[i].name=="PS_Rocks"){TransformrockEffect=ps[i].transform;if(currentPart.name.Contains("0.5")){rockEffect.gameObject.SetActive(false);}else{Vector3pos=(currentPart.position-rockEffect.localPosition).normalized;ps[i].transform.localPosition+=pos*4f;}}else{shape.radius-=2.3f;}}// Parts with names containing "0.5" are half the size and the effect arc needs adjustingif(currentPart.name.Contains("0.5")){// Default arc is 60 so this one needs to be halfshape.arc=30f;// and half the offsetoffset=15f;}}// Set the rotation of the effectVector3rotation=newVector3(-90f,0f,currentPart.eulerAngles.y-offset);effect.eulerAngles=rotation;#endregion// Play the effecteffect.GetComponent<ParticleSystem>().Play();CameraShake.Shake(0.2f,0.3f);while(timer<=shakeTime){Vector3endPos=currentPart.right*shakeCurve.Evaluate(timer)+Vector3.up*fallCurve.Evaluate(timer);currentPart.position=Vector3.Lerp(currentPart.position,endPos,shakeSpeed);timer+=Time.deltaTime;yieldreturnnull;}yieldreturnStartCoroutine(Fall(currentPart));}privateIEnumeratorFall(TransformcurrentPart){floattimer=0.0f;while(timer<=fallTime){currentPart.position=Vector3.Lerp(currentPart.position,Vector3.down*10f,fallSpeed*Time.deltaTime);timer+=Time.deltaTime;yieldreturnnull;}currentPart.parent.gameObject.SetActive(false);}

publicstaticclassConvertWorldToScreenPosition{/// <summary>/// Get a screen position from a world position relative to the parent/// </summary>/// <param name="parent"> RectTransform that the UI element should be displayed within. </param>/// <param name="worldPosition"> The position that should be converted. </param>/// <param name="min"> Bottom Left of the level. </param>/// <param name="max"> Top Right of the level. </param>/// <returns> The position converted. </returns>publicstaticVector2GetPosition(RectTransformparent,Vector3worldPosition,Vector3min,Vector3max){Vector2pos;pos.x=GetPos(worldPosition.x,parent.sizeDelta.x,GetLevelSize(min,max).x);pos.y=GetPos(worldPosition.z,parent.sizeDelta.y,GetLevelSize(min,max).y);returnpos;}privatestaticfloatGetPos(floatpos,floatmapSize,floatlevelSize){returnpos*(mapSize/levelSize);}privatestaticVector2GetLevelSize(Vector3min,Vector3max){returnnewVector2(max.x-min.x,max.z-min.z);}}