Hp Bar
특별한 에셋을 사용하지 않을 것이고 Ui를 이용한 것이 아닌 그냥 2D sprite를 이용할 것이다.
하이어라키 창은 다음과 같다.
여기서 Bar의 X 스케일을 조절하면 hp가 줄어든 것처럼 보이게할 수 있다. 이는 실제 체력과 연동해야하기 때문에 스크립트를 작성해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HpBar : MonoBehaviour
{
[SerializeField]
Transform _hpBar = null;
public void SetHpBar(float ratio)
{
ratio = Mathf.Clamp(ratio, 0, 1);
_hpBar.localScale = new Vector3(ratio, 1, 1);
}
}
이제 컨트롤러에서 이 HpBar 프리팹을 생성하고 SetHpBar를 호출해주면 된다.
protected void AddHpBar()
{
GameObject go = Managers.Resource.Instantiate("UI/HpBar", transform);
go.transform.localPosition = new Vector3(0, 0.5f, 0);
go.name = "HpBar";
_hpBar = go.GetComponent<HpBar>();
UpdateHpBar();
}
void UpdateHpBar()
{
if(_hpBar == null )
{
return;
}
float ratio = 0.0f;
if(Stat.MaxHp > 0)
{
ratio = ((float)Hp / Stat.MaxHp);
}
_hpBar.SetHpBar(ratio);
}
모든 객체는 Init 함수에서 AddHpBar를 호출해주면된다.
DieEffect
이제 체력바가 보이니 체력이 0이 되면 죽는 이펙트를 부활시켜보자. 즉 Server의 OnDead함수에 내용을 채워보자.
message S_Die {
int32 objectId = 1;
int32 attackerId = 2;
}
패킷을 만들어서 OnDead함수를 채워준다.
public virtual void OnDead(GameObject attacker)
{
S_Die diePacket = new S_Die();
diePacket.ObjectId = Id;
diePacket.AttackerId = attacker.Id;
Room.Broadcast(diePacket);
GameRoom room = Room;
room.LeaveGame(Id);
Stat.Hp = Stat.MaxHp;
PosInfo.State = CreatureState.Idle;
PosInfo.PosX = 0;
PosInfo.PosY = 0;
room.EnterGame(this);
}
죽게되면 방에서 나갔다가 다시 돌아오는 형식으로 구현했다.
public static void S_DieHandler(PacketSession session, IMessage packet)
{
S_Die diePacket = packet as S_Die;
GameObject go = Managers.Object.FindById(diePacket.ObjectId);
if (go == null)
return;
CreatureController cc = go.GetComponent<CreatureController>();
if (cc != null)
{
cc.Hp = 0;
cc.OnDead();
}
}
public virtual void OnDead()
{
State = CreatureState.Dead;
GameObject effect = Managers.Resource.Instantiate("Effect/DieEffect");
effect.transform.position = transform.position;
effect.GetComponent<Animator>().Play("START");
GameObject.Destroy(effect, 0.5f);
}
클라이언트는 이렇게 처리해주었다.
Search AI
우리가 예전에 작업했던 몬스터 AI를 이번에는 서버에서 동작할 수 있게 수정해보자.
using Google.Protobuf.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game
{
public class Monster : GameObject
{
public Monster()
{
ObjectType = GameObjectType.Monster;
Stat.Level = 1;
Stat.Hp = 100;
Stat.MaxHp = 100;
Stat.Speed = 5;
State = CreatureState.Idle;
}
// FSM (Finite State Machine)
public override void Update()
{
switch (State)
{
case CreatureState.Idle:
UpdateIdle();
break;
case CreatureState.Moving:
UpdateMoving();
break;
case CreatureState.Skill:
UpdateSkill();
break;
case CreatureState.Dead:
UpdateDead();
break;
}
}
Player _target;
int _searchCellDist = 10;
int _chaseCellDist = 20;
long _nextSearchTick = 0;
protected virtual void UpdateIdle()
{
if (_nextSearchTick > Environment.TickCount64)
return;
_nextSearchTick = Environment.TickCount64 + 1000;
Player target = Room.FindPlayer(p =>
{
Vector2Int dir = p.CellPos - CellPos;
return dir.cellDistFromZero <= _searchCellDist;
});
if (target == null)
{
return;
}
_target = target;
State = CreatureState.Moving;
}
long _nextMoveTick = 0;
protected virtual void UpdateMoving()
{
if (_nextMoveTick > Environment.TickCount64)
return;
int moveTick = (int)(1000 / Speed);
_nextMoveTick = Environment.TickCount64 + moveTick;
if(_target == null || _target.Room != Room)
{
_target = null;
State = CreatureState.Idle;
return;
}
int dist = (_target.CellPos - CellPos).cellDistFromZero;
if(dist == 0 || dist > _chaseCellDist)
{
_target = null;
State = CreatureState.Idle;
return;
}
List<Vector2Int> path = Room.Map.FindPath(CellPos, _target.CellPos, checkObjects : false);
if (path.Count < 2 || path.Count > _chaseCellDist)
{
_target = null;
State = CreatureState.Idle;
return;
}
// 이동
Dir = GetDirFormVec(path[1]-CellPos);
Room.Map.ApplyMove(this, path[1]);
// 다른 플레이어한테도 알려준다.
S_Move movePacket = new S_Move();
movePacket.ObjectId = Id;
movePacket.PosInfo = PosInfo;
Room.Broadcast(movePacket);
}
protected virtual void UpdateSkill()
{
}
protected virtual void UpdateDead()
{
}
}
}
Skill AI
이제 플레이어를 쫓아가는 것 까지 구현 됐으니 스킬 범위 안에 플레이어가 있을 때 스킬을 사용하게 해보자.
using Google.Protobuf.Protocol;
using Server.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace Server.Game
{
public class Monster : GameObject
{
public Monster()
{
ObjectType = GameObjectType.Monster;
Stat.Level = 1;
Stat.Hp = 100;
Stat.MaxHp = 100;
Stat.Speed = 5;
State = CreatureState.Idle;
}
// FSM (Finite State Machine)
public override void Update()
{
switch (State)
{
case CreatureState.Idle:
UpdateIdle();
break;
case CreatureState.Moving:
UpdateMoving();
break;
case CreatureState.Skill:
UpdateSkill();
break;
case CreatureState.Dead:
UpdateDead();
break;
}
}
Player _target;
int _searchCellDist = 10;
int _chaseCellDist = 20;
long _nextSearchTick = 0;
protected virtual void UpdateIdle()
{
if (_nextSearchTick > Environment.TickCount64)
return;
_nextSearchTick = Environment.TickCount64 + 1000;
Player target = Room.FindPlayer(p =>
{
Vector2Int dir = p.CellPos - CellPos;
return dir.cellDistFromZero <= _searchCellDist;
});
if (target == null)
{
return;
}
_target = target;
State = CreatureState.Moving;
}
int _skillRange = 1;
long _nextMoveTick = 0;
protected virtual void UpdateMoving()
{
if (_nextMoveTick > Environment.TickCount64)
return;
int moveTick = (int)(1000 / Speed);
_nextMoveTick = Environment.TickCount64 + moveTick;
if(_target == null || _target.Room != Room)
{
_target = null;
State = CreatureState.Idle;
BroadcastMove();
return;
}
Vector2Int dir = _target.CellPos - CellPos;
int dist = dir.cellDistFromZero;
if(dist == 0 || dist > _chaseCellDist)
{
_target = null;
State = CreatureState.Idle;
BroadcastMove();
return;
}
List<Vector2Int> path = Room.Map.FindPath(CellPos, _target.CellPos, checkObjects : false);
if (path.Count < 2 || path.Count > _chaseCellDist)
{
_target = null;
State = CreatureState.Idle;
BroadcastMove();
return;
}
// 스킬로 넘어갈지 체크
if(dist <= _skillRange && (dir.x == 0 || dir.y == 0))
{
_coolTick = 0;
State = CreatureState.Skill;
return;
}
// 이동
Dir = GetDirFormVec(path[1]-CellPos);
Room.Map.ApplyMove(this, path[1]);
BroadcastMove();
}
void BroadcastMove()
{
// 다른 플레이어한테도 알려준다.
S_Move movePacket = new S_Move();
movePacket.ObjectId = Id;
movePacket.PosInfo = PosInfo;
Room.Broadcast(movePacket);
}
long _coolTick = 0;
protected virtual void UpdateSkill()
{
if(_coolTick == 0)
{
// 유요한 타켓인지
if(_target == null || _target.Room != Room || _target.Hp < 0)
{
_target = null;
State = CreatureState.Moving;
BroadcastMove();
return;
}
// 스킬이 아직 사용 가능한지
Vector2Int dir = (_target.CellPos - CellPos);
int dist = dir.cellDistFromZero;
bool canUseSkill = (dist <= _skillRange && (dir.x == 0 || dir.y == 0));
if(canUseSkill == false)
{
State = CreatureState.Moving;
BroadcastMove();
return;
}
// 타겟팅 방향 주시
MoveDir lookDir = GetDirFormVec(dir);
if(Dir != lookDir)
{
Dir = lookDir;
BroadcastMove();
}
// 데미지 판정
Skill skillData = null;
DataManager.SkillDict.TryGetValue(1, out skillData);
_target.OnDamaged(this, skillData.damage + Stat.Attack);
// 스킬사용 Broadcast
S_Skill skill = new S_Skill() { Info = new SkillInfo() };
skill.ObjectId = Id;
skill.Info.SkillId = skillData.id;
Room.Broadcast(skill);
// 스킬 쿨타임 적용
int coolTick = (int)(1000 * skillData.cooldown);
_coolTick = Environment.TickCount64 + coolTick;
}
if (_coolTick > Environment.TickCount64)
return;
_coolTick = 0;
}
protected virtual void UpdateDead()
{
}
}
}
이렇게 해서 우리가 연습하려고 했던 내용은 얼추 마무리했다. 그런데 문제는 지금 서버에서 lock을 너무 남발해서 사용하긴 해서 로직이 매우 더럽게 구성되어 있다. 지금은 플레이어가 몇 없게때문에 괜찮지만 유저가 많아지면 분명 엄청난 경합이 일어날 것이다.
다음 포스팅에는 서버 로직을 다르게 수정해보자.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] DB 연동 - Player, HP db 연동 (0) | 2024.03.26 |
---|---|
[Unity 2D] 서버 구조 변경 - Command 패턴과 Job (0) | 2024.03.14 |
[Unity 2D] 서버 연동 - Data & Config 과 피격 (0) | 2024.03.12 |
[Unity 2D] 서버 연동 - 스킬 동기화와 히트 판정 (0) | 2024.03.11 |
[Unity 2D] 서버 연동 - MyPlayer 분리 및 이동 동기화 (0) | 2024.03.08 |