Unity 2D 플랫폼 게임에 적 추가
Unity에서 플랫폼 게임을 만드는 것은 상대적으로 쉽지만 기능적인 적을 추가하는 것은 그리 간단하지 않을 수 있습니다.
이번 포스팅에서는 적 AI로 2D 플랫폼 게임을 만드는 방법을 보여드리겠습니다.
일반적으로 2D 플랫폼 게임에서 플레이어는 앞/뒤로 걷고, 점프하고, 지도가 다층인 경우 경우에 따라 사다리를 오르거나 내릴 수만 있습니다. 플레이어와 AI 간에 동일한 컨트롤러를 공유하는 모듈식 접근 방식을 사용할 수 있다는 것을 알고 있습니다.
1단계: 스크립트 생성
필요한 모든 스크립트를 작성하는 것부터 시작해 보겠습니다. 아래 소스 코드를 확인하세요.
Ladder2D.cs
//Copyright @2018 sharpcoderblog.com
//You are free to use this script in free or commercial projects
//Selling the source code of this script is not allowed
using UnityEngine;
public class Ladder2D : MonoBehaviour
{
Collider2D ladderCollider;
[HideInInspector]
public Vector3 boundsCenter;
void Start()
{
//t = transform;
ladderCollider = GetComponent<Collider2D>();
if (ladderCollider)
{
ladderCollider.isTrigger = true;
ladderCollider.gameObject.layer = 2; //Set ladder collider layer to IgnoreRaycast
}
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
if (ladderCollider)
{
boundsCenter = ladderCollider.bounds.center;
}
other.SendMessage("AssignLadder", this, SendMessageOptions.DontRequireReceiver);
}
}
void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
other.SendMessage("RemoveLadder", this, SendMessageOptions.DontRequireReceiver);
}
}
}
PlayerController2D.cs
//Copyright @2018 sharpcoderblog.com
//You are free to use this script in free or commercial projects
//Selling the source code of this script is not allowed
using System.Collections.Generic;
using UnityEngine;
public class PlayerController2D : MonoBehaviour
{
//Move player in 2D space
public float maxSpeed = 2.57f;
public float jumpHeight = 6.47f;
public float playerHP = 100;
[HideInInspector]
public bool facingRight = true;
[HideInInspector]
public float moveDirection = 0;
[HideInInspector]
public Rigidbody2D r2d;
[HideInInspector]
public Collider2D mainCollider;
[HideInInspector]
public Vector2 playerDimensions;
[HideInInspector]
public bool isGrounded = false;
//Check every collider except Player and Ignore Raycast
LayerMask layerMask = ~(1 << 2 | 1 << 8); //Make sure our player has Layer 8
[HideInInspector]
public Ladder2D currentLadder;
List<Ladder2D> allLadders = new List<Ladder2D>();
float moveDirectionY = 0;
float distanceFromLadder;
[HideInInspector]
public bool isAttachedToLadder = false;
bool ladderGoingDown = false;
//bool isMovingOnLadder = false;
[HideInInspector]
public bool canGoDownOnLadder = false;
[HideInInspector]
public bool canClimbLadder = false;
//Bot movement directions
[HideInInspector]
public bool isBot = false;
[HideInInspector]
public float botMovement = 0;
[HideInInspector]
public float botVerticalMovement = 0;
[HideInInspector]
public bool botJump = false;
[HideInInspector]
public Transform t;
[HideInInspector]
public int selectedWeaponTmp = 0;
float gravityScale;
// Use this for initialization
void Start()
{
r2d = GetComponent<Rigidbody2D>();
r2d.freezeRotation = true;
mainCollider = GetComponent<Collider2D>();
t = transform;
gravityScale = r2d.gravityScale;
selectedWeaponTmp = -100;
facingRight = t.localScale.x > 0;
//sr = GetComponent<SpriteRenderer>();
playerDimensions = BotController2D.ColliderDimensions(GetComponent<Collider2D>());
}
void OnDisable()
{
r2d.bodyType = RigidbodyType2D.Static;
r2d.velocity = Vector3.zero;
}
// Update is called once per frame
void Update()
{
if (!isBot)
{
if ((Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D)) &&
(isGrounded || r2d.velocity.x > 0.01f || isAttachedToLadder))
{
moveDirection = Input.GetKey(KeyCode.A) ? -1 : 1;
}
else
{
if (isGrounded || r2d.velocity.magnitude < 0.01f)
moveDirection = 0;
}
}
else
{
if (botMovement != 0 && (isGrounded || r2d.velocity.x > 0.01f))
{
moveDirection = botMovement < 0 ? -1 : 1;
}
else
{
if (isGrounded || r2d.velocity.magnitude < 0.01f)
moveDirection = 0;
}
}
//Change facing position
if (moveDirection != 0)
{
if (moveDirection > 0 && !facingRight)
{
facingRight = true;
}
if (moveDirection < 0 && facingRight)
{
facingRight = false;
}
}
if (facingRight)
{
if (t.localScale.x < 0)
{
t.localScale = new Vector3(Mathf.Abs(t.localScale.x), t.localScale.y, transform.localScale.z);
}
}
else
{
if (t.localScale.x > 0)
{
t.localScale = new Vector3(-Mathf.Abs(t.localScale.x), t.localScale.y, t.localScale.z);
}
}
//Vector2 velocityTmp = r2d.velocity;
bool canGoDownTmp = false;
// LADDER CONTROL START
if (currentLadder)
{
distanceFromLadder = Mathf.Abs(currentLadder.boundsCenter.x - t.position.x);
canClimbLadder = distanceFromLadder < 0.34f;
if (!isAttachedToLadder)
{
if (canClimbLadder)
{
if (currentLadder.boundsCenter.y > t.position.y)
{
if (!isBot)
{
if (Input.GetKey(KeyCode.W))
{
isAttachedToLadder = true;
}
}
else
{
if (botVerticalMovement > 0)
{
isAttachedToLadder = true;
}
}
}
if (currentLadder.boundsCenter.y < t.position.y)
{
if (!isBot)
{
if (Input.GetKey(KeyCode.S))
{
isAttachedToLadder = true;
}
}
else
{
if (botVerticalMovement < 0)
{
isAttachedToLadder = true;
}
}
canGoDownTmp = true;
}
}
if (isAttachedToLadder)
{
r2d.gravityScale = 0;
moveDirection = 0;
moveDirectionY = 0;
}
}
else
{
//Make our collider trigger if we stand on top of the ladder (To prevent collision with the ground while going down)
mainCollider.isTrigger = currentLadder.boundsCenter.y < t.position.y;
//Ladder movement
if ((!isBot && Input.GetKey(KeyCode.W)) || (isBot && botVerticalMovement > 0))
{
moveDirectionY = 3.97f;
ladderGoingDown = false;
//For sound controller
//isMovingOnLadder = true;
}
else if ((!isBot && Input.GetKey(KeyCode.S)) || (isBot && botVerticalMovement < 0))
{
moveDirectionY = -3.97f;
ladderGoingDown = true;
if (!mainCollider.isTrigger && isGrounded)
{
//RemoveLadder(currentLadder);
isAttachedToLadder = false;
mainCollider.isTrigger = false;
r2d.gravityScale = gravityScale;
moveDirectionY = 0;
}
//For sound controller
//isMovingOnLadder = true;
}
else
{
//isMovingOnLadder = false;
moveDirectionY = 0;
}
}
if (distanceFromLadder > playerDimensions.x * 2)
{
RemoveLadder(currentLadder);
}
}
canGoDownOnLadder = canGoDownTmp;
// LADDER CONTROL END
if (!isBot)
{
//Jumping
if (Input.GetKeyDown(KeyCode.W))
{
Jump();
}
}
else
{
if (botJump)
{
botJump = false;
Jump();
}
}
if (!isBot)
{
//Weapon firing
if (Input.GetKeyDown(KeyCode.LeftControl))
{
Attack();
}
}
}
void FixedUpdate()
{
Bounds colliderBounds = mainCollider.bounds;
Vector3 groundCheckPos = colliderBounds.min + new Vector3(colliderBounds.size.x * 0.5f, 0.1f, 0);
//Check if player is grounded
isGrounded = Physics2D.OverlapCircle(groundCheckPos, 0.25f, layerMask);
Debug.DrawLine(groundCheckPos, groundCheckPos - new Vector3(0, 0.25f, 0), isGrounded ? Color.green : Color.red);
//Apply player velocity
r2d.velocity = new Vector2((moveDirection) * maxSpeed, isAttachedToLadder ? moveDirectionY : r2d.velocity.y);
}
void AssignLadder(Ladder2D ladderTmp)
{
currentLadder = ladderTmp;
allLadders.Add(ladderTmp);
}
void RemoveLadder(Ladder2D ladderTmp)
{
//print("On trigger out");
allLadders.Remove(ladderTmp);
if (currentLadder == ladderTmp)
{
currentLadder = null;
if (allLadders.Count > 0)
{
currentLadder = allLadders[allLadders.Count - 1];
}
}
if (isAttachedToLadder && !currentLadder)
{
isAttachedToLadder = false;
//r2d.bodyType = RigidbodyType2D.Dynamic;
mainCollider.isTrigger = false;
r2d.gravityScale = gravityScale;
r2d.velocity = Vector3.zero;
if (!ladderGoingDown)
{
r2d.velocity = new Vector2(r2d.velocity.x, 1.47f);
}
ladderGoingDown = false;
}
}
public void Jump()
{
if (isGrounded && !isAttachedToLadder)
{
r2d.velocity = new Vector2(r2d.velocity.x, jumpHeight);
//Tip: Play jump sound here
}
}
public void Attack()
{
print(gameObject.name + " is Attacking");
//Tip: Write your attack function here (ex. Raycast toward the enemy to inflict the damage)
}
}
CameraFollow2D.cs
//Copyright @2018 sharpcoderblog.com
//You are free to use this script in free or commercial projects
//Selling the source code of this script is not allowed
using UnityEngine;
public class CameraFollow2D : MonoBehaviour
{
public Transform target;
public Vector3 offset = new Vector3(0, 2.8f, 0);
public bool smoothFollow = true;
Vector2 moveToPos;
bool beginMove = false;
float distanceTmp = 0f;
// Update is called once per frame
void LateUpdate()
{
if (!target)
return;
if (smoothFollow)
{
distanceTmp = ((target.position + offset) - transform.position).sqrMagnitude;
if (beginMove)
{
moveToPos = Vector3.Lerp(moveToPos, target.position + offset, Time.fixedDeltaTime * 7.75f);
transform.position = new Vector3(moveToPos.x, moveToPos.y, -10);
if (distanceTmp < 0.05f * 0.05f)
{
beginMove = false;
}
}
else
{
if (distanceTmp > 0.5f * 0.5f)
{
beginMove = true;
}
}
}
else
{
transform.position = new Vector3(target.position.x, target.position.y, -10);
}
}
void StopFollowing()
{
beginMove = false;
}
}
BotController2D.cs
//Copyright @2018 sharpcoderblog.com
//You are free to use this script in free or commercial projects
//Selling the source code of this script is not allowed
using System.Collections;
using UnityEngine;
public class BotController2D : MonoBehaviour
{
//This script will handle bot control
public enum BotType { Enemy, Friendly }
public BotType botType = BotType.Enemy;
public enum BotDifficulty { Easy, Medium, Hard }
public BotDifficulty botDifficulty = BotDifficulty.Medium;
public enum InitialState { Idle, Explore }
public InitialState initialState = InitialState.Idle; //Should the Bot stand in place until approached or begin exploring the level right away
public bool canJump = true; //Can this bot jump?
public enum CurrentState { Idle, MovingLeft, MovingRight, GoinUPLadder, GoingDownLadder, Attack }
CurrentState currentState;
InitialState appliedState;
PlayerController2D pc2d;
RaycastHit2D hitLeft;
RaycastHit2D hitRight;
RaycastHit2D groundHit;
Vector3 leftOrigin;
Vector3 rightOrigin;
float distanceLeft = -1;
float distanceRight = -1;
int encounteredLadders = 0;
int encounteredLaddersCacche = 0;
Ladder2D previousLadder;
Ladder2D lastAttachedLadder;
bool previousCanGoDownOnLadder = false;
bool previousCanClimbLadder = false;
//Everything except "Player" and "IgnoreRaycast" layers
LayerMask layerMask = ~(1 << 2 | 1 << 8);
//Only "Player" layer
LayerMask playerLayerMask = 1 << 8;
float timeMotionless = 0.0f;
bool statePause = false;
float trVelocity;
Vector3 previousPos;
Collider2D[] detectedPlayers = new Collider2D[0];
Collider2D[] previousDetectedPlayers = new Collider2D[0];
PlayerController2D enemyToFollow;
int followPriority = 0; //0 = Easy, 1 = Medium (This player inflicted the damage)
[HideInInspector]
public Transform t;
bool checkingTotalEnemies = false;
bool runAway = false;
int attackingFromLeft = 0;
int attackingFromRight = 0;
//Limit attack rate for easy bots
float attackTimer = 0;
float nextAttackTime = 0;
Camera mainCamera;
float cameraWidth; //Horizontal size of camera view
// Use this for initialization
void Start()
{
pc2d = GetComponent<PlayerController2D>();
pc2d.isBot = true;
t = transform;
appliedState = initialState;
if (Random.Range(-10, 10) > 0)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
}
// Update is called once per frame
void FixedUpdate()
{
//Draw rays back and forth
if (!mainCamera)
{
mainCamera = Camera.main;
cameraWidth = mainCamera.aspect * mainCamera.orthographicSize;
}
rightOrigin = t.position + t.right * (pc2d.playerDimensions.x / 2f);
hitRight = Physics2D.Raycast(rightOrigin, t.right, cameraWidth, layerMask);
if (hitRight)
{
Debug.DrawLine(rightOrigin, hitRight.point, Color.red);
distanceRight = hitRight.distance;
}
else
{
Debug.DrawLine(rightOrigin, rightOrigin + t.right * cameraWidth, Color.cyan);
distanceRight = -1;
}
leftOrigin = t.position - t.right * (pc2d.playerDimensions.x / 2f);
hitLeft = Physics2D.Raycast(leftOrigin, -t.right, cameraWidth, layerMask);
if (hitLeft)
{
Debug.DrawLine(leftOrigin, hitLeft.point, Color.red);
distanceLeft = hitLeft.distance;
}
else
{
Debug.DrawLine(leftOrigin, leftOrigin - t.right * cameraWidth, Color.cyan);
distanceLeft = -1;
}
if (appliedState == InitialState.Explore)
{
if (currentState == CurrentState.Idle)
{
if (!statePause)
{
//Decide which direction to move
if (distanceRight == -1 && distanceLeft == -1)
{
//Decide random direaction
currentState = Random.Range(-10, 10) > 0 ? CurrentState.MovingRight : CurrentState.MovingLeft;
}
else if (distanceRight == -1 && distanceLeft >= 0)
{
currentState = CurrentState.MovingRight;
}
else if (distanceRight >= 0 && distanceLeft == -1)
{
currentState = CurrentState.MovingLeft;
}
else if (distanceRight > distanceLeft)
{
currentState = CurrentState.MovingRight;
}
else if (distanceRight < distanceLeft)
{
currentState = CurrentState.MovingLeft;
}
}
}
else if (currentState == CurrentState.MovingLeft)
{
if (!statePause && pc2d.isGrounded)
{
pc2d.botMovement = -1;
float jumpHeightTmp = pc2d.jumpHeight * 0.25f;
if (distanceLeft > 0 && distanceLeft < pc2d.playerDimensions.x)
{
if (hitLeft && canJump &&
!Physics2D.Linecast(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) - t.right * pc2d.playerDimensions.x * 2, layerMask) &&
Random.Range(-2, 10) > 0
)
{
StartCoroutine(DoJump());
}
else
{
if (!enemyToFollow)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
}
}
/*if(!Physics2D.Linecast(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) - t.right * pc2d.playerDimensions.x * 2)){
Debug.DrawLine(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) - t.right * pc2d.playerDimensions.x * 2, Color.yellow);
}
else
{
Debug.DrawLine(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) - t.right * pc2d.playerDimensions.x * 2, Color.red);
}*/
//Jump if there is no groun in front
groundHit = Physics2D.Raycast(leftOrigin, -t.up, pc2d.playerDimensions.y * 2.1f, layerMask);
if (groundHit)
{
Debug.DrawLine(leftOrigin, groundHit.point, Color.red);
}
else
{
Debug.DrawLine(leftOrigin, leftOrigin - t.up * (pc2d.playerDimensions.y * 2.1f), Color.blue);
if (canJump)
{
StartCoroutine(DoJump());
}
else
{
//StartCoroutine(StatePause(CurrentState.MovingRight, true));
StartCoroutine(CheckEnemiesEnumerator(1, 0, false));
}
}
}
}
else if (currentState == CurrentState.MovingRight)
{
if (!statePause && pc2d.isGrounded)
{
pc2d.botMovement = 1;
float jumpHeightTmp = pc2d.jumpHeight * 0.25f;
if (distanceRight > 0 && distanceRight < pc2d.playerDimensions.x)
{
if (hitRight && canJump &&
!Physics2D.Linecast(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) + t.right * pc2d.playerDimensions.x * 2, layerMask) &&
Random.Range(-2, 10) > 0
)
{
StartCoroutine(DoJump());
}
else
{
if (!enemyToFollow)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
}
}
/*if (!Physics2D.Linecast(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) + t.right * pc2d.playerDimensions.x * 2))
{
Debug.DrawLine(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) + t.right * pc2d.playerDimensions.x * 2, Color.yellow);
}
else
{
Debug.DrawLine(t.position + t.up * jumpHeightTmp, (t.position + t.up * jumpHeightTmp) + t.right * pc2d.playerDimensions.x * 2, Color.red);
}*/
//Jump if there is no groun in front
groundHit = Physics2D.Raycast(rightOrigin, -t.up, pc2d.playerDimensions.y * 2.1f, layerMask);
if (groundHit)
{
Debug.DrawLine(rightOrigin, groundHit.point, Color.red);
}
else
{
Debug.DrawLine(rightOrigin, rightOrigin - t.up * (pc2d.playerDimensions.y * 2.1f), Color.blue);
if (canJump)
{
StartCoroutine(DoJump());
}
else
{
//StartCoroutine(StatePause(CurrentState.MovingLeft, true));
StartCoroutine(CheckEnemiesEnumerator(0, 1, false));
}
}
}
}
else if (currentState == CurrentState.GoinUPLadder)
{
if (!statePause)
{
pc2d.botVerticalMovement = 1;
if (!pc2d.currentLadder)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
}
}
else if (currentState == CurrentState.GoingDownLadder)
{
if (!statePause)
{
pc2d.botVerticalMovement = -1;
if (!pc2d.currentLadder)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
}
}
else if (currentState == CurrentState.Attack)
{
if (!statePause)
{
if (!enemyToFollow)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
else
{
//Firing weapon
if (attackTimer >= nextAttackTime)
{
//Check if player is above us and jump
if (enemyToFollow.t.position.y > t.position.y && enemyToFollow.t.position.y - t.position.y > pc2d.playerDimensions.y * 0.95f)
{
if (Random.Range(-5, 10) > 0)
{
StartCoroutine(DoJump());
}
}
//
pc2d.Attack();
if (botDifficulty == BotDifficulty.Easy)
{
attackTimer = 0;
nextAttackTime = Random.Range(0.25f, 0.95f);
}
if (botDifficulty == BotDifficulty.Medium)
{
attackTimer = 0;
nextAttackTime = Random.Range(0.01f, 0.37f);
}
if (botDifficulty == BotDifficulty.Hard)
{
attackTimer = 0;
nextAttackTime = Random.Range(0.01f, 0.24f);
}
}
else
{
attackTimer += Time.deltaTime;
}
if (enemyToFollow && !checkingTotalEnemies)
{
if (enemyToFollow.t.position.x > t.position.x && !pc2d.facingRight)
{
pc2d.facingRight = true;
}
if (enemyToFollow.t.position.x < t.position.x && pc2d.facingRight)
{
pc2d.facingRight = false;
}
attackingFromLeft = 0;
attackingFromRight = 0;
//Check if there too many player attacking us and run away
for (int i = 0; i < detectedPlayers.Length; i++)
{
if (detectedPlayers[i])
{
BotController2D bcTmp = detectedPlayers[i].GetComponent<BotController2D>();
if (bcTmp && bcTmp.botType != botType && bcTmp.enemyToFollow == pc2d && bcTmp.currentState == CurrentState.Attack)
{
if (bcTmp.t.position.x > t.position.x)
{
attackingFromRight++;
}
else
{
attackingFromLeft++;
}
}
}
}
//If the value playerHP from PlayerController2D get too low, and the bot is being attacked, increase the probability to run away
if (attackingFromRight >= 2 || attackingFromLeft >= 2 || (pc2d.playerHP < 70 && botDifficulty == BotDifficulty.Hard && (attackingFromRight > 0 || attackingFromLeft > 0)) || (pc2d.playerHP < 40 && botDifficulty == BotDifficulty.Medium && (attackingFromRight > 0 || attackingFromLeft > 0)))
{
StartCoroutine(CheckEnemiesEnumerator(attackingFromLeft, attackingFromRight, false));
}
}
}
}
}
}
if (pc2d.currentLadder && (previousLadder != pc2d.currentLadder || previousCanGoDownOnLadder != pc2d.canGoDownOnLadder || previousCanClimbLadder != pc2d.canClimbLadder))
{
previousLadder = pc2d.currentLadder;
previousCanGoDownOnLadder = pc2d.canGoDownOnLadder;
previousCanClimbLadder = pc2d.canClimbLadder;
if (!pc2d.isAttachedToLadder)
{
if (pc2d.canClimbLadder)
{
encounteredLadders++;
if ((lastAttachedLadder != pc2d.currentLadder || encounteredLadders > 1) && !statePause)
{
if (Random.Range(-10, 10) > 0)
{
if (pc2d.canGoDownOnLadder)
{
StartCoroutine(StatePause(CurrentState.GoingDownLadder, true));
}
else
{
StartCoroutine(StatePause(CurrentState.GoinUPLadder, true));
}
}
}
}
}
else
{
encounteredLadders = 0;
lastAttachedLadder = pc2d.currentLadder;
}
}
trVelocity = ((t.position - previousPos).magnitude) / Time.deltaTime;
previousPos = t.position;
if (trVelocity < 0.01f && !statePause)
{
timeMotionless += Time.deltaTime;
if (timeMotionless > 0.5f)
{
StartCoroutine(StatePause(CurrentState.Idle, true));
}
}
else
{
timeMotionless = 0;
}
//Detect and attack enemy players
detectedPlayers = Physics2D.OverlapCircleAll(t.position, cameraWidth, playerLayerMask);
if (!enemyToFollow)
{
if (!runAway)
{
if (previousDetectedPlayers.Length != detectedPlayers.Length || (previousDetectedPlayers.Length > 0 && detectedPlayers.Length > 0 && previousDetectedPlayers[0] != detectedPlayers[0]))
{
previousDetectedPlayers = detectedPlayers;
for (int i = 0; i < detectedPlayers.Length; i++)
{
BotController2D bcTmp = detectedPlayers[i].GetComponent<BotController2D>();
PlayerController2D pc2dTmp = null;
if (!bcTmp)
{
pc2dTmp = detectedPlayers[i].GetComponent<PlayerController2D>();
}
if ((pc2dTmp && botType == BotType.Enemy) || (bcTmp && bcTmp.botType != botType))
{
Vector3 enemyPos = bcTmp ? bcTmp.t.position : pc2dTmp.t.position;
float yDistance = Mathf.Abs(enemyPos.y - t.position.y);
if (yDistance < pc2d.playerDimensions.y * 2)
{
if (!enemyToFollow || Mathf.Abs(enemyPos.x - t.position.x) < Mathf.Abs(enemyToFollow.t.position.x - t.position.x))
{
enemyToFollow = bcTmp ? bcTmp.pc2d : pc2dTmp;
appliedState = InitialState.Explore;
}
}
}
}
}
}
}
else
{
float yDistance = enemyToFollow.t.position.y - t.position.y;
float xDistance = enemyToFollow.t.position.x - t.position.x;
if (Mathf.Abs(yDistance) >= pc2d.playerDimensions.y * 2 || Mathf.Abs(xDistance) > cameraWidth || !enemyToFollow.enabled)
{
enemyToFollow = null;
}
else
{
if (Mathf.Abs(xDistance) > pc2d.playerDimensions.x * 1.45f)
{
if (!statePause && pc2d.botVerticalMovement == 0)
{
if (xDistance > 0)
{
if (currentState != CurrentState.MovingRight)
{
statePauseCoroutine = StartCoroutine(StatePause(CurrentState.MovingRight, false));
}
}
else if (xDistance < 0)
{
if (currentState != CurrentState.MovingLeft)
{
statePauseCoroutine = StartCoroutine(StatePause(CurrentState.MovingLeft, false));
}
}
}
else
{
StopPauseCoroutine();
}
}
else
{
if (pc2d.botVerticalMovement == 0)
{
if (currentState != CurrentState.Attack)
{
if (!statePause)
{
statePauseCoroutine = StartCoroutine(StatePause(CurrentState.Attack, true));
}
}
else
{
if (Mathf.Abs(xDistance) < pc2d.playerDimensions.x / 3 && !checkingTotalEnemies)
{
//print("Enemies are too close!!!");
StartCoroutine(CheckEnemiesEnumerator(attackingFromLeft, attackingFromRight, true));
}
}
}
}
}
}
}
Coroutine statePauseCoroutine = null;
void StopPauseCoroutine()
{
if (statePauseCoroutine != null)
{
StopCoroutine(statePauseCoroutine);
statePauseCoroutine = null;
statePause = false;
}
}
IEnumerator StatePause(CurrentState newState, bool stopMovement)
{
//print("State pause");
statePause = true;
if (stopMovement)
{
pc2d.botMovement = 0;
pc2d.botVerticalMovement = 0;
}
currentState = newState;
if (newState == CurrentState.Attack && botDifficulty == BotDifficulty.Hard)
{
yield return new WaitForSeconds(Random.Range(0.15f, 0.45f));
}
else
{
yield return new WaitForSeconds(Random.Range(0.45f, 0.75f));
}
statePause = false;
}
IEnumerator DoJump()
{
//print("Do jump");
statePause = true;
pc2d.botJump = true;
yield return new WaitForSeconds(0.65f);
statePause = false;
}
IEnumerator CheckEnemiesEnumerator(int attackingFromLeft, int attackingFromRight, bool doNotRunAway)
{
checkingTotalEnemies = true;
//print("CHECKING FOR TOTAL ENEMIES");
yield return new WaitForSeconds(Random.Range(0.27f, 0.75f));
if (Random.Range(-10, 10) > 0)
{
runAway = true;
enemyToFollow = null;
if (attackingFromLeft > attackingFromRight)
{
currentState = CurrentState.MovingRight;
}
else
{
currentState = CurrentState.MovingLeft;
}
}
checkingTotalEnemies = false;
if (runAway)
{
if (doNotRunAway)
{
//Simply walk away a bit
yield return new WaitForSeconds(Random.Range(0.37f, 0.75f));
}
else
{
//Run away
yield return new WaitForSeconds(Random.Range(1.57f, 2.45f));
}
runAway = false;
}
}
public static Vector2 ColliderDimensions(Collider2D sp)
{
return new Vector2(sp.bounds.max.x - sp.bounds.min.x, sp.bounds.max.y - sp.bounds.min.y);
}
}
2단계: 플레이어와 적 설정
이제 위의 스크립트를 사용하여 플레이어와 적 AI를 설정할 차례입니다.
플레이어 인스턴스 설정
- 새로운 GameObject를 생성하고 이름을 지정하세요. "Player"
- 해당 개체의 태그를 다음으로 변경합니다. "Player"
- 객체의 레이어를 8로 변경합니다(선택 항목에 레이어 8이 없으면 레이어 추가...를 클릭하여 추가하고 이름을 "Player"로 지정).
- 다른 GameObject를 만들고 이름을 "Body"로 지정하고 SpriteRenderer 구성 요소를 추가합니다.
- 플레이어 스프라이트를 "Body"에 할당하고 "Player" 개체 내부로 이동합니다.
- "Player" 개체를 선택하고 CapsuleCollider2D, Rigidbody2D 및 PlayerController2D 구성 요소를 추가합니다.
- 플레이어 Sprite에 맞을 때까지 CapsuleCollider2D의 크기를 조정합니다.
보시다시피 PlayerController2D에는 몇 가지 변수가 있으며 대부분은 설명이 필요합니다. 그러나 그 중 하나에는 약간의 설명이 필요합니다.
PlayerHP - 이 값은 BotController2D에서 HP가 너무 낮을 때 AI가 도망가야 하는지 여부를 결정하는 데 사용됩니다.
공격 기능을 구현할 때 PlayerHP 변수를 사용하십시오(PlayerController2D.cs 스크립트 끝에서 void Attack() 확인).
플레이어 카메라 설정
- 기본 카메라를 선택하고 CameraFollow2D 구성 요소를 추가합니다.
- 플레이어를 Target 변수에 할당
- 선택적으로 오프셋 변수를 조정할 수 있습니다(카메라가 정확히 중앙에 위치하는 것을 원하지 않는 경우).
사다리 설치하기
PlayerController2D는 올라갈 수 있는 사다리도 지원합니다.
새 래더를 설정하는 것은 정말 쉽습니다.
- 새로운 GameObject를 생성하고 호출합니다. "Ladder"
- 레이어를 다음으로 변경하세요. "IgnoreRaycast"
- SpriteRenderer를 사용하여 다른 게임 개체를 만들고 사다리의 스프라이트를 할당한 다음 Ladder 개체 내부로 이동합니다.
- "Ladder" 개체에 BoxCollider2D 및 Ladder2D 구성 요소를 추가합니다.
- 사다리 스프라이트와 일치하도록 충돌기 크기의 크기를 조정하고 트리거로 표시합니다.
적 AI 설정
- 먼저 Physics2D 패널로 이동하여 Player 레이어 간의 충돌을 비활성화하여 봇과 플레이어가 서로 끼지 않도록 합니다.
- 플레이어 인스턴스 복제
- BotController2D 구성 요소 추가
- 이제 봇이 준비되었습니다
enemy AI가 작동하는 모습을 보려면 아래 동영상을 확인하세요. (흰색 인스턴스는 플레이어이고, 빨간색 인스턴스는 AI에 의해 제어됩니다.)