티스토리 뷰
오늘은 상속을 더욱 응용시켜 기능을 만들어보도록 하자. 상속의 특징 중에서 먼저 override를 살펴보자.
저번 상속을 설명하며 작성했던 코드에서 변경사항이 생겼다. 법사가 새로 공중부양을 하여 이동하는 기능이 생겼고, 이로 인해 코드를 변경해야 한다. 하지만 다른 직업은 걸어다니는 이동을 수행해야 하기 때문에 CharacterEx 클래스의 Move() 메소드를 변경하면 모든 직업들이 공중부양을 하게 되므로 MagicianEx 클래스 내에서 변경해야 한다.
이 때 자식 클래스에서 부모 클래스의 Move() 메소드를 변경할 수 있는 기능이 있다. 바로 override 기능이다. 자식 클래스에서 부모 클래스의 메소드를 확장 정의 / 수정 정의 / 재정의할 수 있는 기능으로, 보통 부모 클래스의 메소드에 필수적으로 virtual 키워드를 작성하고 자식 클래스에서는 override 키워드를 작성하여 메소드를 재정의한다. 이렇게 재정의하게 되면 기사 클래스나 NPC 클래스에서는 부모 메소드의 Move() 메소드를 따르게 되고, 법사 클래스는 법사 클래스에서 재정의한 Move() 메소드를 따르게 되는 것이다. 자식 클래스에서 부모 클래스의 메소드를 덮어버린다고 생각하면 편하다.
아래에는 virtual과 override 키워드를 사용하여 재작성한 코드이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 상속 문법을 사용한 캐릭터 클래스들
// 부모 클래스를 이용한 상속 코드 구현 순서 (일단 중복 해결에 대한 기준으로만)
// 1. 중복되는 멤버 변수와 메소드를 공통(부모 클래스)으로 이동시킴
// 2. 중복된 내용 제거된(부모 클래스로 옮겨진) 자식 클래스에서 부모 클래스를 상속받는 형식의 명세로 변경
// -> public 자식 클래스 : 부모 클래스 { ...중복이 제거되고 남은 코드들... }
// 3.
public class CharacterEx // 부모 클래스
{
// * protected : 자식 클래스에서 부모 클래스의 멤버변수(속성) 및 메소드를 접근 허용하고 싶을 때 사용하는 접근 지정자
// 이름
protected string name;
// 이동속도
protected float speed;
// 공격력
protected int damage;
// 체력
protected int hp;
public CharacterEx(string name, float speed, int damage, int hp = 1) // 부모 클래스 생성자
{
this.name = name;
this.speed = speed;
this.damage = damage;
this.hp = hp;
}
// 자식 클래스에서 Move 메소드를 확장정의/수정정의/재정의할 수 있도록 virtual 키워드를 붙여줘야 함.
// * 목적 : 부모 클래스로부터 물려받은 메소드가 자식 클래스의 역할/기능에 맞지 않을 경우
// 이동 메소드
public virtual void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 발바닥에 땀이 나도록 뛰면서 이동을 수행합니다.");
}
// 공격 기능
public void Attack()
{
Debug.Log("[" + name + "] 캐릭터가 " + damage + "로 공격을 수행합니다.");
}
// 피격 메소드
public void Hit(int damage)
{
hp -= damage;
if (hp < 0)
hp = 0;
Debug.Log("[" + name + "] 캐릭터가 " + damage + "만큼의 공격을 받아 체력이 " + hp + "이 되었습니다.");
}
}
// NPC
public class NPCEx : CharacterEx // NPCEx가 CharacterEx 클래스를 상속받음
{
// 대화내용
private string talkMessage;
// * NPC가 생성될 때 넘겨받은 생성자 매개변수의 값을 부모의 생성자에게 넘겨줌
// -> 문법 : 자식 생성자(매개변수를 ...) : base(부모 생성자 초기화 전달값)
public NPCEx(string name, float speed, int damage, string talkMessage) : base(name, speed, damage)
{
this.talkMessage = talkMessage;
}
// 대화 수행 메소드
public void Talk()
{
Debug.Log("[" + name + "] 캐릭터가 " + talkMessage + "로 유저들에게 말을 겁니다.");
}
}
// 기사 클래스
public class KnightEx : CharacterEx
{
// 힘(방패 치기 기술 공격력)
private int str;
// 기사 생성자
public KnightEx(string name, float speed, int damage, int str, int hp) : base(name, speed, damage, hp)
{
this.str = str;
}
// 방패 치기 메소드
public void ShieldAttack()
{
Debug.Log("[" + name + "] 캐릭터가 방패로 " + str + "만큼의 힘으로 밀쳐 냅니다.");
}
}
// 마법사 클래스
public class MagicianEx : CharacterEx
{
// 지능(파이어볼 기술 공격력)
private int inte;
// 법사 생성자
public MagicianEx(string name, float speed, int damage, int inte, int hp) : base(name, speed, damage, hp)
{
this.inte = inte;
}
// 자식 클래스는 부모가 재정의를 허용한 메소드에 대한 재정의를 위해 override 키워드를 넣어 메소드를 정의해줘야함
public override void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 공중부양해서 하늘을 날아다니며 이동을 수행합니다.");
}
// 파이어볼 메소드
public void FireballAttack()
{
Debug.Log("[" + name + "] 캐릭터가 마법으로 " + inte + "만큼의 지능으로 파이어볼을 발사합니다.");
}
}
public class ExtendComponent : MonoBehaviour
{
void Start()
{
NPC npc = new NPC("NPC", 10, 5, "찾으시는 물건이 있으십니까?");
npc.Move();
npc.Attack();
npc.Hit(1);
npc.Talk();
Debug.Log("------------------------------------------");
Knight knight = new Knight("방패기사", 20, 100, 30, 1000);
knight.Move(); // 기사가 이동을 수행함
knight.Attack(); // 기사가 공격을 수행함
knight.Hit(120); // 기사가 피격당함
knight.ShieldAttack(); // 방패치기 공격 수행
Debug.Log("------------------------------------------");
Magician magician = new Magician("해리포터마법사", 15, 200, 100, 300);
magician.Move(); // 법사가 이동을 수행함
magician.Attack(); // 법사가 공격을 수행함
magician.Hit(120); // 법사가 피격당함
magician.FireballAttack(); // 파이어볼 공격 수행
Debug.Log("------------------------------------------");
}
}
또 하나의 가상의 변경 사항을 만들어보자. 기사 직업의 성능이 안좋다고 판단하여 공격을 할 때 특수 스킬로 부여했던 방패 치기 기술까지 쓰게 하려고 한다. 대신 유저가 직접 방패 치기 기술을 사용하는 것은 막기로 한다. 이런 상황에서는 메소드에도 private 지정자를 사용하고 Attack() 메소드를 override하여 아래와 같이 작성할 수 있다.
또한 이러한 경우에는, 부모의 Attack() 메소드를 사용하면서 추가로 방패 치기 메소드까지 사용하는 것이니 base 키워드를 통하여 base.Attack()을 작성하여 부모의 메소드를 실행한 후 ShieldAttack() 메소드를 실행하여 모두 실행한 모습을 볼 수 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 상속 문법을 사용한 캐릭터 클래스들
// 부모 클래스를 이용한 상속 코드 구현 순서 (일단 중복 해결에 대한 기준으로만)
// 1. 중복되는 멤버 변수와 메소드를 공통(부모 클래스)으로 이동시킴
// 2. 중복된 내용 제거된(부모 클래스로 옮겨진) 자식 클래스에서 부모 클래스를 상속받는 형식의 명세로 변경
// -> public 자식 클래스 : 부모 클래스 { ...중복이 제거되고 남은 코드들... }
// 3.
public class CharacterEx // 부모 클래스
{
// * protected : 자식 클래스에서 부모 클래스의 멤버변수(속성) 및 메소드를 접근 허용하고 싶을 때 사용하는 접근 지정자
// 이름
protected string name;
// 이동속도
protected float speed;
// 공격력
protected int damage;
// 체력
protected int hp;
public CharacterEx(string name, float speed, int damage, int hp = 1) // 부모 클래스 생성자
{
this.name = name;
this.speed = speed;
this.damage = damage;
this.hp = hp;
}
// 자식 클래스에서 Move 메소드를 확장정의/수정정의/재정의할 수 있도록 virtual 키워드를 붙여줘야 함.
// * 목적 : 부모 클래스로부터 물려받은 메소드가 자식 클래스의 역할/기능에 맞지 않을 경우
// 이동 메소드
public virtual void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 발바닥에 땀이 나도록 뛰면서 이동을 수행합니다.");
}
// 공격 기능
public virtual void Attack()
{
Debug.Log("[" + name + "] 캐릭터가 " + damage + "로 공격을 수행합니다.");
}
// 피격 메소드
public void Hit(int damage)
{
hp -= damage;
if (hp < 0)
hp = 0;
Debug.Log("[" + name + "] 캐릭터가 " + damage + "만큼의 공격을 받아 체력이 " + hp + "이 되었습니다.");
}
}
// NPC
public class NPCEx : CharacterEx // NPCEx가 CharacterEx 클래스를 상속받음
{
// 대화내용
private string talkMessage;
// * NPC가 생성될 때 넘겨받은 생성자 매개변수의 값을 부모의 생성자에게 넘겨줌
// -> 문법 : 자식 생성자(매개변수를 ...) : base(부모 생성자 초기화 전달값)
public NPCEx(string name, float speed, int damage, string talkMessage) : base(name, speed, damage)
{
this.talkMessage = talkMessage;
}
// 대화 수행 메소드
public void Talk()
{
Debug.Log("[" + name + "] 캐릭터가 " + talkMessage + "로 유저들에게 말을 겁니다.");
}
}
// 기사 클래스
public class KnightEx : CharacterEx
{
// 힘(방패 치기 기술 공격력)
private int str;
// 기사 생성자
public KnightEx(string name, float speed, int damage, int str, int hp) : base(name, speed, damage, hp)
{
this.str = str;
}
public override void Attack()
{
// 자식 클래스에서 재정의(override) 하면서 호출되지 않는 부모의 메소드의 동작이 필요할 경우)
// 자식 클래스에서 base 키워드를 통해 호출되지 않는 부모의 메소드를 인위적으로 호출해줘야 함
base.Attack();
ShieldAttack();
}
// 방패 치기 메소드
private void ShieldAttack()
{
Debug.Log("[" + name + "] 캐릭터가 방패로 " + str + "만큼의 힘으로 밀쳐 냅니다.");
}
}
// 마법사 클래스
public class MagicianEx : CharacterEx
{
// 지능(파이어볼 기술 공격력)
private int inte;
// 법사 생성자
public MagicianEx(string name, float speed, int damage, int inte, int hp) : base(name, speed, damage, hp)
{
this.inte = inte;
}
// 자식 클래스는 부모가 재정의를 허용한 메소드에 대한 재정의를 위해 override 키워드를 넣어 메소드를 정의해줘야함
public override void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 공중부양해서 하늘을 날아다니며 이동을 수행합니다.");
}
// 파이어볼 메소드
public void FireballAttack()
{
Debug.Log("[" + name + "] 캐릭터가 마법으로 " + inte + "만큼의 지능으로 파이어볼을 발사합니다.");
}
}
public class ExtendComponent : MonoBehaviour
{
void Start()
{
NPC npc = new NPC("NPC", 10, 5, "찾으시는 물건이 있으십니까?");
npc.Move();
npc.Attack();
npc.Hit(1);
npc.Talk();
Debug.Log("------------------------------------------");
Knight knight = new Knight("방패기사", 20, 100, 30, 1000);
knight.Move(); // 기사가 이동을 수행함
knight.Attack(); // 기사가 공격을 수행함
knight.Hit(120); // 기사가 피격당함
Debug.Log("------------------------------------------");
Magician magician = new Magician("해리포터마법사", 15, 200, 100, 300);
magician.Move(); // 법사가 이동을 수행함
magician.Attack(); // 법사가 공격을 수행함
magician.Hit(120); // 법사가 피격당함
magician.FireballAttack(); // 파이어볼 공격 수행
Debug.Log("------------------------------------------");
}
}
마지막으로 사망 로그를 출력하는 Die() 메소드를 작성해보자. 각 직업마다 사망 로그 출력이 다르게 작성하게 작성하고 싶은데, 상속을 응용하려면 어떻게 해야 할까?
먼저, 이 부분에서는 상속 후 덮어쓰기 기능을 사용하면 된다. 상속하는 CharacterEx 클래스에 새롭게 Die() 메소드를 생성한다. 또한 Die() 메소드에는 아무것도 작성하지 않거나 주석으로 남겨 둔다. 이 후, 이 Die() 메소드는 protected virtual 키워드를 사용해서 외부에서는 접근이 되지 않고, Hit() 메소드에서 hp가 0 이하로 떨어지게 되면 호출하게 작성한다. 마지막으로 자식 클래스에서 override하여 클래스마다 사망 로그를 다르게 작성하면 된다.
아래는 Die() 메소드 관련하여 추가 작성한 코드이다.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
// 상속 문법을 사용한 캐릭터 클래스들
// 부모 클래스를 이용한 상속 코드 구현 순서 (일단 중복 해결에 대한 기준으로만)
// 1. 중복되는 멤버 변수와 메소드를 공통(부모 클래스)으로 이동시킴
// 2. 중복된 내용 제거된(부모 클래스로 옮겨진) 자식 클래스에서 부모 클래스를 상속받는 형식의 명세로 변경
// -> public 자식 클래스 : 부모 클래스 { ...중복이 제거되고 남은 코드들... }
// 3.
public class CharacterEx // 부모 클래스
{
// * protected : 자식 클래스에서 부모 클래스의 멤버변수(속성) 및 메소드를 접근 허용하고 싶을 때 사용하는 접근 지정자
// 이름
protected string name;
// 이동속도
protected float speed;
// 공격력
protected int damage;
// 체력
protected int hp;
public CharacterEx(string name, float speed, int damage, int hp = 1) // 부모 클래스 생성자
{
this.name = name;
this.speed = speed;
this.damage = damage;
this.hp = hp;
}
// 자식 클래스에서 Move 메소드를 확장정의/수정정의/재정의할 수 있도록 virtual 키워드를 붙여줘야 함.
// * 목적 : 부모 클래스로부터 물려받은 메소드가 자식 클래스의 역할/기능에 맞지 않을 경우
// 이동 메소드
public virtual void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 발바닥에 땀이 나도록 뛰면서 이동을 수행합니다.");
}
// 공격 기능
public virtual void Attack()
{
Debug.Log("[" + name + "] 캐릭터가 " + damage + "로 공격을 수행합니다.");
}
// 피격 메소드
public void Hit(int damage)
{
hp -= damage;
Debug.Log("[" + name + "] 캐릭터가 " + damage + "만큼의 공격을 받아 체력이 " + hp + "이 되었습니다.");
if (hp < 0)
{
hp = 0;
Die();
}
}
protected virtual void Die()
{
//호출하면 안되는 메소드 자식 클래스들은 각자 구현할 것"
}
}
// NPC
public class NPCEx : CharacterEx // NPCEx가 CharacterEx 클래스를 상속받음
{
// 대화내용
private string talkMessage;
// * NPC가 생성될 때 넘겨받은 생성자 매개변수의 값을 부모의 생성자에게 넘겨줌
// -> 문법 : 자식 생성자(매개변수를 ...) : base(부모 생성자 초기화 전달값)
public NPCEx(string name, float speed, int damage, string talkMessage) : base(name, speed, damage)
{
this.talkMessage = talkMessage;
}
// 대화 수행 메소드
public void Talk()
{
Debug.Log("[" + name + "] 캐릭터가 " + talkMessage + "로 유저들에게 말을 겁니다.");
}
protected override void Die()
{
Debug.Log("꼴까닥 소리를 내며 쓰러져서 사라집니다.");
}
}
// 기사 클래스
public class KnightEx : CharacterEx
{
// 힘(방패 치기 기술 공격력)
private int str;
// 기사 생성자
public KnightEx(string name, float speed, int damage, int str, int hp) : base(name, speed, damage, hp)
{
this.str = str;
}
public override void Attack()
{
// 자식 클래스에서 재정의(override) 하면서 호출되지 않는 부모의 메소드의 동작이 필요할 경우)
// 자식 클래스에서 base 키워드를 통해 호출되지 않는 부모의 메소드를 인위적으로 호출해줘야 함
base.Attack();
ShieldAttack();
}
// 방패 치기 메소드
private void ShieldAttack()
{
Debug.Log("[" + name + "] 캐릭터가 방패로 " + str + "만큼의 힘으로 밀쳐 냅니다.");
}
protected override void Die()
{
Debug.Log("조국의 영광을 위해 지금 쓰러지지만 후회하지는 않는다.");
}
}
// 마법사 클래스
public class MagicianEx : CharacterEx
{
// 지능(파이어볼 기술 공격력)
private int inte;
// 법사 생성자
public MagicianEx(string name, float speed, int damage, int inte, int hp) : base(name, speed, damage, hp)
{
this.inte = inte;
}
// 자식 클래스는 부모가 재정의를 허용한 메소드에 대한 재정의를 위해 override 키워드를 넣어 메소드를 정의해줘야함
public override void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 공중부양해서 하늘을 날아다니며 이동을 수행합니다.");
}
// 파이어볼 메소드
public void FireballAttack()
{
Debug.Log("[" + name + "] 캐릭터가 마법으로 " + inte + "만큼의 지능으로 파이어볼을 발사합니다.");
}
protected override void Die()
{
Debug.Log("난 언젠가 마법의 힘으로 부활할거다. I will be back.");
}
}
public class ExtendComponent : MonoBehaviour
{
void Start()
{
NPCEx npc = new NPCEx("NPC", 10, 5, "찾으시는 물건이 있으십니까?");
npc.Move();
npc.Attack();
npc.Hit(10);
npc.Talk();
Debug.Log("------------------------------------------");
KnightEx knight = new KnightEx("방패기사", 20, 100, 30, 1000);
knight.Move(); // 기사가 이동을 수행함
knight.Attack(); // 기사가 공격을 수행함
knight.Hit(12000); // 기사가 피격당함
Debug.Log("------------------------------------------");
MagicianEx magician = new MagicianEx("해리포터마법사", 15, 200, 100, 300);
magician.Move(); // 법사가 이동을 수행함
magician.Attack(); // 법사가 공격을 수행함
magician.Hit(320); // 법사가 피격당함
magician.FireballAttack(); // 파이어볼 공격 수행
Debug.Log("------------------------------------------");
}
}
실제로 Start() 메소드에서는 Die() 메소드를 호출하지 않았다. 직업 클래스들이 상속받은 Hit() 메소드를 사용해서 hp를 0보다 낮아지게 설정하고, Die() 메소드가 각자 설정한 사망 로그 출력에 맞게 출력되는지 확인해보기 위하여 저렇게 작성하였다. 여기서 protected의 용법도 한 번 다시 상기해보자. 실제로 위와 같은 코드에서는 Start() 메소드가 포함된 ExtendedComponent 클래스에서 npc.Die()와 같은 메소드는 npcEx 클래스와 상속 관계가 아니기 때문에 사용할 수 없게 에러가 난다. Die() 메소드를 외부에서 접근할 수 없지만 public 접근 지정자인 Hit() 메소드를 통하여서만 접근할 수 있게 통제한 것이다.
하지만 여기서 의문
Q : 접근이 불가하게 만들고 싶으면 어짜피 Die() 메소드를 직접 호출하지 않을 것이니 protected가 아닌 private로 사용해도 되지 않나?
A : 그 전 상황에서, Die() 메소드는 재정의를 위해 virtual과 override를 사용한 것을 확인할 수 있다. 접근 지정자는 가상 메소드라는 표현인 virtual 키워드와 아래에서 언급할 추상 메소드인 abstract 키워드를 사용했다면 무조건 접근 지정자는 protected이여야 한다. private로 지정한다고 하더라도 빨간색 밑줄로 에러가 나는 것을 확인할 수 있다.
하지만 여기에는 핵심적인 주의사항이 있다. 현재 Die()라는 메소드에는 아무것도 구현이 되어 있지 않고 모두 자식 클래스에서 override하여 사용하고 있다. 여기에 Healer라는 새로운 클래스를 만들고 혹시라도 Die() 메소드를 재정의하지 않더라도, 스크립트 컴파일 에러나 오류는 뜨지 않는다는 것이다. 실제로 새로운 Healer 클래스를 만들고 Hit() 메소드로 hp를 0 이하로 깎는다고 해도 Healer는 사망 로그가 뜨지 않는 오류가 있는 것을 확인할 수 있다. 아래의 코드가 그러하다.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
// 상속 문법을 사용한 캐릭터 클래스들
// 부모 클래스를 이용한 상속 코드 구현 순서 (일단 중복 해결에 대한 기준으로만)
// 1. 중복되는 멤버 변수와 메소드를 공통(부모 클래스)으로 이동시킴
// 2. 중복된 내용 제거된(부모 클래스로 옮겨진) 자식 클래스에서 부모 클래스를 상속받는 형식의 명세로 변경
// -> public 자식 클래스 : 부모 클래스 { ...중복이 제거되고 남은 코드들... }
// 3.
public class CharacterEx // 부모 클래스
{
// * protected : 자식 클래스에서 부모 클래스의 멤버변수(속성) 및 메소드를 접근 허용하고 싶을 때 사용하는 접근 지정자
// 이름
protected string name;
// 이동속도
protected float speed;
// 공격력
protected int damage;
// 체력
protected int hp;
public CharacterEx(string name, float speed, int damage, int hp = 1) // 부모 클래스 생성자
{
this.name = name;
this.speed = speed;
this.damage = damage;
this.hp = hp;
}
// 자식 클래스에서 Move 메소드를 확장정의/수정정의/재정의할 수 있도록 virtual 키워드를 붙여줘야 함.
// * 목적 : 부모 클래스로부터 물려받은 메소드가 자식 클래스의 역할/기능에 맞지 않을 경우
// 이동 메소드
public virtual void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 발바닥에 땀이 나도록 뛰면서 이동을 수행합니다.");
}
// 공격 기능
public virtual void Attack()
{
Debug.Log("[" + name + "] 캐릭터가 " + damage + "로 공격을 수행합니다.");
}
// 피격 메소드
public void Hit(int damage)
{
hp -= damage;
Debug.Log("[" + name + "] 캐릭터가 " + damage + "만큼의 공격을 받아 체력이 " + hp + "이 되었습니다.");
if (hp < 0)
{
hp = 0;
// 부모 클래스에서 메소드를 호출할 때 현재 객체가 자식 객체로 생성됐을 경우에는 자식의 오버라이드된 메소드가 호출된다.
Die();
}
}
protected virtual void Die()
{
//호출하면 안되는 메소드 자식 클래스들은 각자 구현할 것
}
}
// NPC
public class NPCEx : CharacterEx // NPCEx가 CharacterEx 클래스를 상속받음
{
// 대화내용
private string talkMessage;
// * NPC가 생성될 때 넘겨받은 생성자 매개변수의 값을 부모의 생성자에게 넘겨줌
// -> 문법 : 자식 생성자(매개변수를 ...) : base(부모 생성자 초기화 전달값)
public NPCEx(string name, float speed, int damage, string talkMessage) : base(name, speed, damage)
{
this.talkMessage = talkMessage;
}
// 대화 수행 메소드
public void Talk()
{
Debug.Log("[" + name + "] 캐릭터가 " + talkMessage + "로 유저들에게 말을 겁니다.");
}
protected override void Die()
{
Debug.Log("꼴까닥 소리를 내며 쓰러져서 사라집니다.");
}
}
public class Healer : CharacterEx
{
private int healPoint; // 체력 보충 수치
public Healer(string name, float speed, int damage, int healPoint, int hp) : base(name, speed, damage, hp)
{
this.healPoint = healPoint;
}
}
// 기사 클래스
public class KnightEx : CharacterEx
{
// 힘(방패 치기 기술 공격력)
private int str;
// 기사 생성자
public KnightEx(string name, float speed, int damage, int str, int hp) : base(name, speed, damage, hp)
{
this.str = str;
}
public override void Attack()
{
// 자식 클래스에서 재정의(override) 하면서 호출되지 않는 부모의 메소드의 동작이 필요할 경우)
// 자식 클래스에서 base 키워드를 통해 호출되지 않는 부모의 메소드를 인위적으로 호출해줘야 함
base.Attack();
ShieldAttack();
}
// 방패 치기 메소드
private void ShieldAttack()
{
Debug.Log("[" + name + "] 캐릭터가 방패로 " + str + "만큼의 힘으로 밀쳐 냅니다.");
}
protected override void Die()
{
Debug.Log("조국의 영광을 위해 지금 쓰러지지만 후회하지는 않는다.");
}
}
// 마법사 클래스
public class MagicianEx : CharacterEx
{
// 지능(파이어볼 기술 공격력)
private int inte;
// 법사 생성자
public MagicianEx(string name, float speed, int damage, int inte, int hp) : base(name, speed, damage, hp)
{
this.inte = inte;
}
// 자식 클래스는 부모가 재정의를 허용한 메소드에 대한 재정의를 위해 override 키워드를 넣어 메소드를 정의해줘야함
public override void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 공중부양해서 하늘을 날아다니며 이동을 수행합니다.");
}
// 파이어볼 메소드
public void FireballAttack()
{
Debug.Log("[" + name + "] 캐릭터가 마법으로 " + inte + "만큼의 지능으로 파이어볼을 발사합니다.");
}
protected override void Die()
{
Debug.Log("난 언젠가 마법의 힘으로 부활할거다. I will be back.");
}
}
public class ExtendComponent : MonoBehaviour
{
void Start()
{
NPCEx npc = new NPCEx("NPC", 10, 5, "찾으시는 물건이 있으십니까?");
npc.Move();
npc.Attack();
npc.Hit(10);
npc.Talk();
Debug.Log("------------------------------------------");
KnightEx knight = new KnightEx("방패기사", 20, 100, 30, 1000);
knight.Move(); // 기사가 이동을 수행함
knight.Attack(); // 기사가 공격을 수행함
knight.Hit(12000); // 기사가 피격당함
Debug.Log("------------------------------------------");
MagicianEx magician = new MagicianEx("해리포터마법사", 15, 200, 100, 300);
magician.Move(); // 법사가 이동을 수행함
magician.Attack(); // 법사가 공격을 수행함
magician.Hit(320); // 법사가 피격당함
magician.FireballAttack(); // 파이어볼 공격 수행
Debug.Log("------------------------------------------");
Healer healer = new Healer("메딕", 10, 0, 50, 50);
healer.Move(); // 힐러가 이동을 수행함
healer.Attack(); // 힐러가 공격을 수행함
healer.Hit(120); // 힐러가 피격당함
// * 힐러는 Die 메소드를 재정의하지 않았기 때문에 부모의 비어있는 Die 메소드가 호출됨(문제점)
// -> 해결법 : 부모 클래스에서 Die 메소드를 자식 클래스에서 반드시 재정의(override)하도록 강제화하면 됨 (abstract)
Debug.Log("------------------------------------------");
}
}
이 점은 상속과 override를 사용할 때는 매우 주의해야 하는 사항이다. 하지만, 개발하는 상황이 오면 개발자는 정신적으로 많이 피폐하기 때문에 이런 부분을 자주 놓칠 수 있다. 그러면 재정의를 필수로 요구하는 키워드는 따로 없을까?
이런 경우에서 쓸 수 있는 키워드가 있다. 바로 abstract이다. abstract 메소드는 추상 메소드로, 자식 클래스에서는 반드시 해당 메소드를 재정의(override)하여야 한다. 또한, 클래스 내부에서 추상 메소드를 사용한다면 해당 메소드가 포함된 클래스도 반드시 abstract 키워드를 사용해야 한다. 이렇게 abstract 키워드를 사용하면, 해당 추상 메소드를 재정의하지 않으면 컴파일 에러가 뜨며 명시해주는 것을 확인할 수 있다. 아래는 abstract 키워드를 사용하여 완성한 스크립트이다.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
// 상속 문법을 사용한 캐릭터 클래스들
// 부모 클래스를 이용한 상속 코드 구현 순서 (일단 중복 해결에 대한 기준으로만)
// 1. 중복되는 멤버 변수와 메소드를 공통(부모 클래스)으로 이동시킴
// 2. 중복된 내용 제거된(부모 클래스로 옮겨진) 자식 클래스에서 부모 클래스를 상속받는 형식의 명세로 변경
// -> public 자식 클래스 : 부모 클래스 { ...중복이 제거되고 남은 코드들... }
// 3.
// CharacterEx 클래스는 추상메소드(Die)를 가진 클래스로 절대로 객체로 생성되면 안되므로 추상 클래스로 정의해야 함.
public abstract class CharacterEx // 부모 클래스
{
// * protected : 자식 클래스에서 부모 클래스의 멤버변수(속성) 및 메소드를 접근 허용하고 싶을 때 사용하는 접근 지정자
// 이름
protected string name;
// 이동속도
protected float speed;
// 공격력
protected int damage;
// 체력
protected int hp;
public CharacterEx(string name, float speed, int damage, int hp = 1) // 부모 클래스 생성자
{
this.name = name;
this.speed = speed;
this.damage = damage;
this.hp = hp;
}
// 자식 클래스에서 Move 메소드를 확장정의/수정정의/재정의할 수 있도록 virtual 키워드를 붙여줘야 함.
// * 목적 : 부모 클래스로부터 물려받은 메소드가 자식 클래스의 역할/기능에 맞지 않을 경우
// 이동 메소드
public virtual void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 발바닥에 땀이 나도록 뛰면서 이동을 수행합니다.");
}
// 공격 기능
public virtual void Attack()
{
Debug.Log("[" + name + "] 캐릭터가 " + damage + "로 공격을 수행합니다.");
}
// 피격 메소드
public void Hit(int damage)
{
hp -= damage;
Debug.Log("[" + name + "] 캐릭터가 " + damage + "만큼의 공격을 받아 체력이 " + hp + "이 되었습니다.");
if (hp < 0)
{
hp = 0;
// 부모 클래스에서 메소드를 호출할 때 현재 객체가 자식 객체로 생성됐을 경우에는 자식의 오버라이드된 메소드가 호출된다.
Die();
}
}
// 외부에서 호출되는 것을 막기위해 protected
// * 내용이 없는 메소드를 만드는게 의미가 있는가?
//protected virtual void Die()
//{
// //호출하면 안되는 메소드 자식 클래스들은 각자 구현할 것
//}
// abstract 메소드는 추상 메소드로 자식 클래스에서는 반드시 Die 메소드를 재정의(override)해야 함
protected abstract void Die();
}
// NPC
public class NPCEx : CharacterEx // NPCEx가 CharacterEx 클래스를 상속받음
{
// 대화내용
private string talkMessage;
// * NPC가 생성될 때 넘겨받은 생성자 매개변수의 값을 부모의 생성자에게 넘겨줌
// -> 문법 : 자식 생성자(매개변수를 ...) : base(부모 생성자 초기화 전달값)
public NPCEx(string name, float speed, int damage, string talkMessage) : base(name, speed, damage)
{
this.talkMessage = talkMessage;
}
// 대화 수행 메소드
public void Talk()
{
Debug.Log("[" + name + "] 캐릭터가 " + talkMessage + "로 유저들에게 말을 겁니다.");
}
protected override void Die()
{
Debug.Log("꼴까닥 소리를 내며 쓰러져서 사라집니다.");
}
}
public class Healer : CharacterEx
{
private int healPoint; // 체력 보충 수치
public Healer(string name, float speed, int damage, int healPoint, int hp) : base(name, speed, damage, hp)
{
this.healPoint = healPoint;
}
protected override void Die()
{
Debug.Log("한 생명이라도 더 구하고 떠나야 하는데 ㅠ");
}
}
// 기사 클래스
public class KnightEx : CharacterEx
{
// 힘(방패 치기 기술 공격력)
private int str;
// 기사 생성자
public KnightEx(string name, float speed, int damage, int str, int hp) : base(name, speed, damage, hp)
{
this.str = str;
}
public override void Attack()
{
// 자식 클래스에서 재정의(override) 하면서 호출되지 않는 부모의 메소드의 동작이 필요할 경우)
// 자식 클래스에서 base 키워드를 통해 호출되지 않는 부모의 메소드를 인위적으로 호출해줘야 함
base.Attack();
ShieldAttack();
}
// 방패 치기 메소드
private void ShieldAttack()
{
Debug.Log("[" + name + "] 캐릭터가 방패로 " + str + "만큼의 힘으로 밀쳐 냅니다.");
}
protected override void Die()
{
Debug.Log("조국의 영광을 위해 지금 쓰러지지만 후회하지는 않는다.");
}
}
// 마법사 클래스
public class MagicianEx : CharacterEx
{
// 지능(파이어볼 기술 공격력)
private int inte;
// 법사 생성자
public MagicianEx(string name, float speed, int damage, int inte, int hp) : base(name, speed, damage, hp)
{
this.inte = inte;
}
// 자식 클래스는 부모가 재정의를 허용한 메소드에 대한 재정의를 위해 override 키워드를 넣어 메소드를 정의해줘야함
public override void Move()
{
Debug.Log("[" + name + "] 캐릭터가 " + speed + "속도로 공중부양해서 하늘을 날아다니며 이동을 수행합니다.");
}
// 파이어볼 메소드
public void FireballAttack()
{
Debug.Log("[" + name + "] 캐릭터가 마법으로 " + inte + "만큼의 지능으로 파이어볼을 발사합니다.");
}
protected override void Die()
{
Debug.Log("난 언젠가 마법의 힘으로 부활할거다. I will be back.");
}
}
public class ExtendComponent : MonoBehaviour
{
void Start()
{
NPCEx npc = new NPCEx("NPC", 10, 5, "찾으시는 물건이 있으십니까?");
npc.Move();
npc.Attack();
npc.Hit(10);
npc.Talk();
Debug.Log("------------------------------------------");
KnightEx knight = new KnightEx("방패기사", 20, 100, 30, 1000);
knight.Move(); // 기사가 이동을 수행함
knight.Attack(); // 기사가 공격을 수행함
knight.Hit(12000); // 기사가 피격당함
Debug.Log("------------------------------------------");
MagicianEx magician = new MagicianEx("해리포터마법사", 15, 200, 100, 300);
magician.Move(); // 법사가 이동을 수행함
magician.Attack(); // 법사가 공격을 수행함
magician.Hit(320); // 법사가 피격당함
magician.FireballAttack(); // 파이어볼 공격 수행
Debug.Log("------------------------------------------");
Healer healer = new Healer("메딕", 10, 0, 50, 50);
healer.Move(); // 힐러가 이동을 수행함
healer.Attack(); // 힐러가 공격을 수행함
healer.Hit(120); // 힐러가 피격당함
Debug.Log("------------------------------------------");
}
}
오늘 학습한 지식들은 개발하며 매우 유용하게 쓰고 자주 쓰는 키워드들과 기술들이니 꼭 숙지해서 익히도록 해야 한다.
'기록하며 노세' 카테고리의 다른 글
인터페이스 (Interface) (2) | 2024.02.08 |
---|---|
업 캐스팅 (Upcasting), 다운 캐스팅 (Downcasting) (2) | 2024.02.08 |
C#에서 상속을 알아보자. (0) | 2024.02.06 |
생성자와 this, overload, Property (0) | 2024.02.05 |
C#에서 메소드(Method)란? (2) | 2024.02.05 |
- Total
- Today
- Yesterday
- 메타버스
- 침해사고분석및대응
- 디폴트매개변수
- 자식클래스
- sbs아카데미게임학원
- 상속
- SpaceShooter
- Abstract
- 유니티
- unity
- 정보보안
- apstndp
- override
- 코루틴
- 물데네전세표응
- virtual
- 가위바위보게임
- 업캐스팅
- 게임리뷰
- 보안
- OOP
- 익숙한출발
- 정보보호
- 정보보호전문가
- 게임개발
- C#
- 핸디커뮤니케이션즈
- 유니티개발
- base
- 부모클래스
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |