Skip to content

Latest commit

 

History

History
630 lines (509 loc) · 17.3 KB

README.md

File metadata and controls

630 lines (509 loc) · 17.3 KB

불멸의 김충선(Immortal Kim Chung Sun)

2학년 1학기 역사 수행평가 (조선 장군 김충선이 왜적을 무찌르는 2D 플래시 액션 게임)


게임 시연

게임 시연 영상


시스템 구조

인 게임 이미지
- 타이틀
image

- 플레이
image
image
image

- 끝
image

image


주요 코드

Actor
    ● 플레이어(김충선)과 적의 모체가 되는 클래스입니다.
    ● abstract 클래스로 상속 후 무조건 공격, 이동, 사망 시 사운드 함수를 구현해야합니다.
    ● 피격, 피격 시 넉백 기능이 있습니다.
자세한 코드
using System.Collections;
using UnityEngine;

// 적과 플레이어(김충선)의 공통점을 담은 추상화 클래스
[RequireComponent(typeof(SpriteRenderer), typeof(Rigidbody2D))]
public abstract class Actor : MonoBehaviour
{
  [Header("Value")]
  [SerializeField] protected int hp;
  [SerializeField] protected int damage;
  [SerializeField] protected float speed;
  [SerializeField] protected float attackRange;
  [SerializeField] protected float attackDelay;
  [SerializeField] protected float knockBackRange;

  protected bool hit = false;

  protected float countAttackDelay = 0f;

  protected Animator animator;
  protected SpriteRenderer spriteRenderer;
  protected Rigidbody2D rigidbody2d;
  protected AudioSource audioSource;

  private Coroutine knockBackCoroutine = null;

  // 자식의 기능 추가를 위해 가상화
  protected virtual void Awake()
  {
  	spriteRenderer = GetComponent<SpriteRenderer>();
  	rigidbody2d = GetComponent<Rigidbody2D>();
  	animator = GetComponent<Animator>();
  	audioSource = GameObject.Find("AudioSource").GetComponent<AudioSource>();
  }

  // 자식의 기능 추가를 위해 가상화
  protected virtual void Update()
  {
  	// 피격 시 공격이 가능하기까지 남은 시간이 줄어들지 않음
  	if (hit) return;

  	if (countAttackDelay > 0)
  	{
  		countAttackDelay -= Time.deltaTime;
  	}

  	Move();

  	Attack();
  }

  // 넉백 애니메이션 및 피격한 대상의 반대 방향으로 밀려남
  private IEnumerator KnockBack()
  {
  	hit = true;

  	animator.SetBool("KnockBack", true);

  	rigidbody2d.AddForce(new Vector2(spriteRenderer.flipX ? -knockBackRange : knockBackRange, 0), ForceMode2D.Impulse);

  	yield return new WaitForSeconds(0.2f);

  	animator.SetBool("KnockBack", false);

  	hit = false;

  	knockBackCoroutine = null;
  }

  // 오브젝트의 사망과 함께 소리 출력
  protected abstract void DeathSound();

  // 오브젝트의 이동
  protected abstract void Move();

  // 다른 오브젝트에서 이 함수를 호출하면 넉백 및 체력 감소
  public void BeShot(int damage)
  {
  	// 전역변수에 현재 실행하고 있는 코루틴을 담아 코루틴 중복 실행 방지
  	if (knockBackCoroutine != null)
  	{
  		StopCoroutine(knockBackCoroutine);
  	}
  	knockBackCoroutine = StartCoroutine(KnockBack());

  	hp -= damage;

  	if (hp <= 0)
  	{
  		DeathSound();
  		Destroy(gameObject);
  	}
  }
  
  // 오브젝트의 공격
  protected abstract void Attack();
}

Player
    ● 플레이어의 이동 및 공격을 담당하는 클래스입니다.
    ● 이동, 공격, 점프, 체력 UI 표현 등의 기능이 있습니다.

자세한 코드
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

// 추상화 클래스 Actor 상속
public class Player : Actor
{
  [SerializeField] private float jumpRange;
  public bool canJump = false;

  [Header("Cashing")]
  [SerializeField] private AudioClip attackSound;
  [SerializeField] private Image hpBar;
  [SerializeField] private TextMeshProUGUI hpText;
  [SerializeField] private Combo combo;

  private int maxHP;

  protected override void Awake()
  {
  	base.Awake();

  	maxHP = hp;

  	animator = GetComponent<Animator>();

  	audioSource = GameObject.Find("AudioSource").GetComponent<AudioSource>();
  }

  protected override void Update()
  {
  	animator.SetBool("Walk", false);

  	base.Update();

  	Jump();

  	// UI에 HP가 얼마나 남았는지 이미지와 텍스트로 표현
  	hpBar.fillAmount = hp / (float)maxHP;

  	hpText.text = hp + "/" + maxHP;
  }

  // 플레이어의 이동
  protected override void Move()
  {
  	// 왼쪽 화살표 입력 시, 이미지가 왼쪽으로 바라보게 하고 왼쪽 이동
  	if (Input.GetKey(KeyCode.LeftArrow))
  	{
  		spriteRenderer.flipX = true;
  		transform.Translate(speed * Time.deltaTime * Vector2.left);

  		animator.SetBool("Walk", true);
  	}
  	// 오른쪽 화살표 입력 시, 이미지가 오른쪽으로 바라보게 하고 오른쪽 이동
  	if (Input.GetKey(KeyCode.RightArrow))
  	{
  		spriteRenderer.flipX = false;
  		transform.Translate(speed * Time.deltaTime * Vector2.right);

  		animator.SetBool("Walk", true);
  	}
  }

  // 플레이어의 점프
  private void Jump()
  {
  	// 이미 점프를 하면 점프 사용 불가능
  	if (!canJump) return;

  	// 위 화살표 입력 시, 실행
  	if (Input.GetKeyDown(KeyCode.UpArrow))
  	{
  		rigidbody2d.AddForce(new Vector2(0, jumpRange), ForceMode2D.Impulse);
  		canJump = false;
  	}
  }

  protected override void Attack()
  {
  	if (countAttackDelay > 0) return;

  	// A 키가 눌렸을 때
  	if (Input.GetKeyDown(KeyCode.A))
  	{
  		// 공격 딜레이 초기화
  		countAttackDelay = attackDelay;
  		
  		// 애니메이션 출력
  		animator.SetTrigger("Attack");

  		// 오디오 출력
  		audioSource.clip = attackSound;
  		audioSource.time = 0.3f;
  		audioSource.Play();

  		// 공격 판정 생성, 만약 범위 내에 오브젝트가 있다면 boxCast 변수에 저장
  		RaycastHit2D boxCast = Physics2D.BoxCast(transform.position, new Vector2(1, 3f), 0, spriteRenderer.flipX ? Vector2.left : Vector2.right, attackRange, LayerMask.GetMask("Enemy"));
  		// boxCast에 저장된 값이 있다면, 적이 피격된 것으로 판단. 적에게 넉백 및 데미지를 가함
  		if (boxCast.transform != null)
  		{
  			combo.Intactly();
  			boxCast.collider.GetComponent<Enemy>().BeShot(damage);
  		}
  	}
  }

  // [사용하지 않음] 사망 시, 바로 끝 Scene으로 가기 때문
  protected override void DeathSound()
  {

  }

  // 오브젝트 삭제 시, End Scene으로 이동
  private void OnDestroy()
  {
  	SceneManager.LoadScene("End");
  }

  // 오브젝트가 닿았을 시, 발동하는 함수
  private void OnTriggerEnter2D(Collider2D collision)
  {
  	// 바닥에 닿았는지 확인하여 점프 여부를 체크
  	if (collision.CompareTag("Ground"))
  	{
  		canJump = true;
  	}

  	// 가시에 닿으면 체력 감소 및 넉백
  	if (collision.CompareTag("Thorn"))
  	{
  		BeShot(5);
  	}

  	// 총알에 피격 시, 피격 당한 총알 삭제 및 체력 감소, 넉백
  	if (collision.CompareTag("Bullet"))
  	{
  		BeShot(collision.GetComponent<Bullet>().damage);
  		Destroy(collision.gameObject);
  	}
  }

}

Enemy
    ● 적 스크립트의 기틀이 되는 부모 클래스입니다.
    ● 플레이어 좌표 값, 획득 가능한 점수, 공격 여부 등 적에게 필요한 변수 값과 사망 시 사운드 재생 기능이 있습니다.

자세한 코드
using UnityEngine;

public abstract class Enemy : Actor
{
  // 적 사망 시 플레이어가 획득하는 점수
  [SerializeField] private int score;

  [Header("Cashing")]
  // 적 사운드 저장 클래스
  [SerializeField] protected EnemySound enemySound;

  // 적의 공격 여부
  protected bool canAttack = false;

  protected Transform playerTransform;

  protected override void Awake()
  {
  	base.Awake();

  	playerTransform = FindObjectOfType<Player>().transform;
  }

  private void OnDestroy()
  {
  	// 씬이 이동되면 플레이어가 적을 삭제하지 않아도 알아서 적이 삭제된다.
  	// 따라서 플레이어가 있을 때, 즉 게임이 진행 중일 때만 점수를 획득하게 조건문을 달았다.
  	if (playerTransform != null)
  	{
  		Score.score += score;
  	}
  }

  protected override void DeathSound()
  {
  	audioSource.clip = enemySound.death;
  	audioSource.time = 0.5f;
  	audioSource.Play();
  }
}

Enemy1
    ● 보스를 포함한 3마리 적 중 하나의 .
    ● 모든 적은 플레이어를 따라오는 AI가 탑재되어 있습니다.
    ● Enemy1은 칼로 근접공격을 합니다.
    ● 이동 AI, 공격 기능이 있습니다.

자세한 코드
using UnityEngine;

// 추상화 클래스 Enemy 상속
public class Enemy1 : Enemy
{
  /// RayCast로 판정 내에 플레이어가 있는지 확인 후 공격
  protected override void Attack()
  {
  	// 범위를 벗어났다면 함수 종료
  	if (!canAttack) return;

  	// 딜레이가 남아있다면 함수 종료
  	if (countAttackDelay > 0) return;

  	// 공격 시작. 딜레이 초기화
  	countAttackDelay = attackDelay;

  	// 애니메이션 출력
  	animator.SetTrigger("Attack");

  	// 사운드 출력
  	audioSource.PlayOneShot(enemySound.attack[0]);

  	// RayCast로 공격 판정 생성. 공격 판정 내에 플레이어가 있다면 boxCast에 값 저장
  	RaycastHit2D boxCast = Physics2D.BoxCast(transform.position, new Vector2(1f, 2f), 0, spriteRenderer.flipX ? Vector2.right : Vector2.left, attackRange, LayerMask.GetMask("Player"));
  	// boxCast에 값이 저장되어있다면, 플레이어에게 넉백 및 데미지를 가함
  	if (boxCast.transform != null)
  	{
  		boxCast.collider.GetComponent<Player>().BeShot(damage);
  	}
  }

  /// 플레이어를 따라가는 AI를 기반으로 이동
  protected override void Move()
  {
  	float range = playerTransform.position.x - transform.position.x;
  	// 플레이어와의 거리가 공격할 수 있는 거리인지 확인 후 Bool 변수에 저장
  	canAttack = Mathf.Abs(range) <= attackRange;
  	// 플레이어의 위치에 따라 이미지 방향 전환
  	spriteRenderer.flipX = range > 0;
  	// 플레이어를 향해 이동
  	transform.position += new Vector3(spriteRenderer.flipX ? Time.deltaTime : -Time.deltaTime, 0) * speed;
  }
}

DataManager
    ● 입력한 이름과 게임을 플레이하면서 얻은 점수를 서버에 전송하는 클래스입니다.
    ● 이름과 점수 데이터 클래스에 저장, 데이터 클래스 Json 변환, 지정한 URL에 Json 전송 기능이 있습니다.

자세한 코드
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;

public class DataManager : MonoBehaviour
{
  // 이름과 점수를 저장하는 데이터 클래스
  private UserData userData;

  // UserData 클래스에 이름과 점수 저장 후 SendStart 호출
  public void GetData(TextMeshProUGUI textMeshProUGUI)
  {
  	userData = new UserData(textMeshProUGUI.text, Score.score);

  	SendStart();
  }

  public void SendStart()
  {
  	// 인터넷이 연결되어있지 않다면 본 함수 실행 종료
  	if (!CheckInternet.internetConnect) return;

  	// 데이터 클래스 Json으로 변환
  	string json = JsonUtility.ToJson(userData);

  	// URL 주소 설정 후 서버에 Request Post
  	StartCoroutine(Upload("http://10.80.162.73:8080/user/save", json));
  }

  // 서버에 전송 이후 처음 타이틀 화면으로 이동
  private IEnumerator Upload(string URL, string json)
  {
  	// UnityWebRequest를 통해 http 통신
  	// 지정된 URL에 byte 단위로 전송
  	using (UnityWebRequest request = UnityWebRequest.Post(URL, json))
  	{
  		byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
  		request.uploadHandler = new UploadHandlerRaw(jsonToSend);
  		request.downloadHandler = new DownloadHandlerBuffer();
  		request.SetRequestHeader("Content-Type", "application/json");

  		// 코루틴으로 프레임 마다 차례차례 전송
  		yield return request.SendWebRequest();

  		// 유니티 에디터 일 때 로그 출력
#if UNITY_EDITOR
  		if (request.isNetworkError || request.isHttpError)
  		{
  			Debug.Log(request.error);
  		}
  		else
  		{
  			Debug.Log(request.downloadHandler.text);
  		}
#endif
  	}

  	// 타이틀 화면으로 이동
  	SceneManager.LoadScene("Start");
  }
}

Phase
    ● 코루틴과 UnityEvent를 이용하여 게임 페이즈(스테이지)를 관리하는 클래스입니다.
    ● 페이즈 종료, 다음 페이즈 시작, 페이즈 모두 클리어 시 끝 Scene으로 가기 기능이 있습니다.

자세한 코드
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;

public class Phase : MonoBehaviour
{
  // 현재 페이즈
  public static int count = 0;

  [Header("Cashing")]
  [SerializeField] private BoxCollider2D wallCollider;
  [SerializeField] private Transform enemySpawnerTransform;
  // UnityEvent로 변수 내에 StageEnd 함수를 지정해놓아 fazeIsEnd를 Invoke하면 StageEnd 함수가 실행됨
  // 페이즈 종료 이벤트 변수
  public UnityEvent fazeIsEnd = new UnityEvent();
  [SerializeField] private TextMeshProUGUI phaseText;
  
  private Transform playerTransform;

  private void Awake()
  {
  	count = 0;

  	playerTransform = FindObjectOfType<Player>().transform;
  }

  private void Update()
  {
  	// 현재 페이즈를 텍스트로 UI에 출력
  	phaseText.text = "페이즈 : " + (count + 1);
  }

  // 페이즈 종료
  public void StageEnd()
  {
  	// 벽 판정 비활성화
  	wallCollider.isTrigger = true;
  	// 카메라가 플레이어를 따라가도록 설정
  	Camera.main.transform.parent = playerTransform;
  	
  	// 종료한 페이즈가 5번째라면 끝 Scene으로 이동
  	if (count + 1 >= 5)
  	{
  		EndGame();
  	}
  }

  // 페이즈 시작
  public void NextStage()
  {
  	// 페이즈 1 증가
  	count++;

  	// 벽 판정 생성
  	wallCollider.isTrigger = false;
  	// 카메라가 더 이상 플레이어를 따라가지 않게 설정
  	Camera.main.transform.parent = null;
  	Camera.main.transform.position = new Vector3(count * 18.1f, 0, -10);
  	// 벽 판정을 현재 페이즈 위치로 이동
  	if (count == 1)
  	{
  		wallCollider.transform.parent.position = new Vector3(17, 0);
  	}
  	else
  	{
  		wallCollider.transform.parent.position += new Vector3(18.1f, 0);
  	}
  	// 적 생성기를 현재 페이즈 위치로 이동
  	enemySpawnerTransform.position += new Vector3(18.1f, 0);
  }

  // 끝 Scene으로 이동
  private void EndGame()
  {
  	SceneManager.LoadScene("End");
  }
}

EnemySpawner
    ● 코루틴을 이용하여 적을 생성하는 클래스입니다.
    ● 적 생성 후 적이 모두 처치되면 UnityEvent로 Phase의 페이즈 종료 함수 호출 기능이 있습니다.

자세한 코드
using System.Collections;
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
  [Header("Cashing")]
  [SerializeField] private Phase phase;
  public EnemyData[] enemyDatas = new EnemyData[5];
  [SerializeField] private GameObject[] enemys = new GameObject[4];

  private void Start()
  {
  	MakeEnemy();
  }

  // 적 생성
  public void MakeEnemy()
  {
  	// 적 생성 코루틴 호출
  	StartCoroutine(Spawn());
  }

  // 적 생성 후 적이 모두 처치되면 페이즈 종료
  private IEnumerator Spawn()
  {
  	// 맨 처음 페이즈라면 2초 대기
  	if (Phase.count == 0) yield return new WaitForSeconds(2);

  	// Enemy1, Enemy2, Boss 순서대로 1초마다 생성
  	for (int i = 0; i < enemyDatas[Phase.count].enemy1; i++)
  	{
  		Instantiate(enemys[0], transform.position, Quaternion.identity);
  		yield return new WaitForSeconds(1);
  	}

  	for (int i = 0; i < enemyDatas[Phase.count].enemy2; i++)
  	{
  		Instantiate(enemys[1], transform.position, Quaternion.identity);
  		yield return new WaitForSeconds(1);
  	}

  	for (int i = 0; i < enemyDatas[Phase.count].boss; i++)
  	{
  		Instantiate(enemys[2], transform.position, Quaternion.identity);
  		yield return new WaitForSeconds(1);
  	}

  	while (FindObjectsOfType<Enemy>().Length != 0)
  	{
  		yield return null;
  	}

  	// Phase 클래스의 페이즈 종료 이벤트 호출
  	phase.fazeIsEnd.Invoke();
  }
}