介绍
这是我在考研期间挖的第二个坑。
二号坑打算做一个【魔塔+roguelike】类型的小游戏。这种类型可以有各种各样的发挥,当然我时间不一定充足,只能先是把基本的部分先造好。
这是这篇文章的效果图:
是不是对画面很无语。身为灵魂画手的我也很无语,但是只是刚开始的demo嘛。
这一篇文章大概就时完成了这样的事:像魔塔里的方式一样移动。碰到怪物了触发战斗。
一 前期准备
如上面的动图先随便准备几张精灵啦,我懒得去unity商店里和我的电脑里找了,我觉得免费的都一般,都是用来建demo的就没啥区别,不影响后面的部分就行。
- 把图片拖到unity里,做成精灵图片。别忘了调pixels per unit。
- 拖到hierarchy窗口,给2d精灵分别加rigibody2d和box collider 2d组件,后面要用到。
二 像魔塔里的英雄一样移动
如何让我们控制的人像魔塔里的英雄一样移动?我一下就想起了官方案例里的2D-Roguelike。所以我基本上是把人家的代码抠出来用了。不过等我抠完里以后觉得好像直接用dotween插件再自己写个协程更方便。。。
public class Player : MonoBehaviour {
public float moveTime = 0.1f; //Time it will take object to move, in seconds.
public LayerMask blockingLayer; //Layer on which collision will be checked.
private BoxCollider2D boxCollider; //The BoxCollider2D component attached to this object.
private Rigidbody2D rb2D; //The Rigidbody2D component attached to this object.
private float inverseMoveTime; //Used to make movement more efficient.
//Static instance of GameManager which allows it to be accessed by any other script.
public static Player instance = null;
private void Awake()
{
if (instance == null)
instance = this;
else if (instance != this)
Destroy(gameObject);
DontDestroyOnLoad(gameObject);
}
void Start () {
boxCollider = GetComponent<BoxCollider2D>();
rb2D = GetComponent<Rigidbody2D>();
inverseMoveTime = 1f / moveTime;
}
/// <summary>
/// Move takes parameters for x direction, y direction and a RaycastHit2D to check collision.
/// </summary>
/// <param name="xDir"></param>
/// <param name="yDir"></param>
/// <param name="hit"></param>
/// <returns>Move returns true if it is able to move and false if not.</returns>
private bool Move(int xDir, int yDir, out RaycastHit2D hit)
{
Vector2 start = transform.position;
Vector2 end = start + new Vector2(xDir, yDir);
boxCollider.enabled = false;
hit = Physics2D.Linecast(start, end, blockingLayer);
boxCollider.enabled = true;
//Check if anything was hit
if (hit.transform == null)
{
//If nothing was hit, start SmoothMovement co-routine passing in the Vector2 end as destination
StartCoroutine(SmoothMovement(end));
//Return true to say that Move was successful
return true;
}
//If something was hit, return false, Move was unsuccesful.
return false;
}
//Co-routine for moving units from one space to next, takes a parameter end to specify where to move to.
private IEnumerator SmoothMovement(Vector3 end)
{
//Calculate the remaining distance to move based on the square magnitude of the difference between current position and end parameter.
//Square magnitude is used instead of magnitude because it's computationally cheaper.
float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
//While that distance is greater than a very small amount (Epsilon, almost zero):
while (sqrRemainingDistance > float.Epsilon)
{
//Find a new position proportionally closer to the end, based on the moveTime
Vector3 newPostion = Vector3.MoveTowards(rb2D.position, end, inverseMoveTime * Time.deltaTime);
//Call MovePosition on attached Rigidbody2D and move it to the calculated position.
rb2D.MovePosition(newPostion);
//Recalculate the remaining distance after moving.
sqrRemainingDistance = (transform.position - end).sqrMagnitude;
//Return and loop until sqrRemainingDistance is close enough to zero to end the function
yield return null;
}
}
//AttemptMove takes a generic parameter T to specify the type of component we expect our unit to interact with if blocked (Player for Enemies, Wall for Player).
private void AttemptMove<T>(int xDir, int yDir)
where T : Component
{
//Hit will store whatever our linecast hits when Move is called.
RaycastHit2D hit;
//Set canMove to true if Move was successful, false if failed.
bool canMove = Move(xDir, yDir, out hit);
//Check if nothing was hit by linecast
if (hit.transform == null)
{ //If nothing was hit, return and don't execute further code.
GameManager.instance.playersTurn = false;
Debug.Log("hit.transform == null");
return;
}
//Get a component reference to the component of type T attached to the object that was hit
T hitComponent = hit.transform.GetComponent<T>();
//If canMove is false and hitComponent is not equal to null, meaning MovingObject is blocked and has hit something it can interact with.
if (!canMove && hitComponent != null)
//Call the OnCantMove function and pass it hitComponent as a parameter.
OnCantMove(hitComponent);
GameManager.instance.playersTurn = false;
}
//The abstract modifier indicates that the thing being modified has a missing or incomplete implementation.
//OnCantMove will be overriden by functions in the inheriting classes.
private void OnCantMove<T>(T component)
where T : Component
{
Debug.Log("OnCantMove");
//获取触碰物组件
//Set hitWall to equal the component passed in as a parameter.
Enemy enemy = component as Enemy;
//触发函数
//Call the Fight function of the Wall we are hitting.
enemy.Fight();
}
void Update () {
if (!GameManager.instance.playersTurn) return;
int horizontal = 0; //Used to store the horizontal move direction.
int vertical = 0; //Used to store the vertical move direction.
horizontal = (int)(Input.GetAxisRaw("Horizontal"));
vertical = (int)(Input.GetAxisRaw("Vertical"));
if (horizontal != 0)
{
vertical = 0;
}
if (horizontal != 0 || vertical != 0)
{
//Call AttemptMove passing in the generic parameter Wall, since that is what Player may interact with if they encounter one (by attacking it)
//Pass in horizontal and vertical as parameters to specify the direction to move Player in.
AttemptMove<Enemy>(horizontal, vertical);
}
}
}
这个脚本就是用来控制移动的,写完拖到角色身上,有个LayerMask blockingLayer设置一下。如果是在玩家可以移动的回合,输入移动键就会试图移动,发射射线检测移动方向一个单位内有没有碰撞,如果没有就移动并且切换成玩家暂休回合(0.2秒),如果有碰撞,玩家无法移动,是碰到怪物的话,就进入战斗,触发enemy.Fight()函数,到第五部分留意一下。
代码有些乱,有冗余。为了省时间就没怎么整理,考研要紧。
接下来是玩家的暂休间隔。有这个间隔可以尽量避免玩家因手滑而造成的gg。写在gamemanager里了,这部分没啥,主要就是协程。理解了协程很好用。
public class GameManager : MonoBehaviour
{
//Time to wait before starting level, in seconds.
public float levelStartDelay = 2f;
//Delay between each Player turn.
public float turnDelay = 0.1f;
//Static instance of GameManager which allows it to be accessed by any other script.
public static GameManager instance = null;
//Boolean to check if it's players turn, hidden in inspector but public.
[HideInInspector] public bool playersTurn = true;
private bool doingSetup = false;
private bool intermission;
private void Awake()
{
if (instance == null)
instance = this;
else if (instance != this)
Destroy(gameObject);
DontDestroyOnLoad(gameObject);
}
// Use this for initialization
void Start()
{
}
void Update()
{
//Check that playersTurn or enemiesMoving or doingSetup are not currently true.
if (playersTurn || intermission || doingSetup)
//If any of these are true, return and do not start MoveEnemies.
return;
//Start moving enemies.
StartCoroutine(WaitForIntermission());
}
IEnumerator WaitForIntermission()
{
intermission = true;
yield return new WaitForSeconds(turnDelay);
playersTurn = true;
intermission = false;
}
public void GameOver()
{
}
三 角色属性
我没写在player脚本里,另写了一个attributes脚本,写完绑到角色player身上。
public class Attributes : MonoBehaviour
{
public int HP
{
get { return _HP; }
set {
_HP = value;
if (_HP <= 0)
{
GameManager.instance.GameOver();
FightEvent.instance.panel.gameObject.SetActive(false);
}
if (_HP >= MaxHP)
{
_HP = MaxHP;
}
}
}
private int _HP=100;
private int MaxHP=1000;
public int Atk=1;
public int Def=0;
// Use this for initialization
void Start()
{
}
}
四 战斗面板
创建一个如图所示的面板。不喜欢可以以后再调整。fightingPanel是panel,hero和enemy是ui-image,下面的都是text。五 敌人和战斗计算
先上代码
public class Enemy : MonoBehaviour
{
public int HP
{
get { return _HP; }
set
{
_HP = value;
if (_HP <= 0)
{
FightEvent.instance.panel.gameObject.SetActive(false);
StopCoroutine(Fighting());
this.gameObject.SetActive(false);
}
}
}
private int _HP=100;
public int Atk=1;
public int Def=0;
private bool playerTurn=true;
private bool enemyTurn=false;
private bool countAtk = true;//计算伤害间隔开关
private bool isFighting = false;//战斗协程开关
private Attributes attributes=null;
void Start()
{
}
void Awake()
{
attributes =GameObject.FindWithTag("Player").GetComponent<Attributes>();
}
// Update is called once per frame
void Update()
{
if(isFighting)
StartCoroutine(Fighting());
}
public void Fight()
{
isFighting = true;//战斗协程开关
GameManager.instance.playersTurn = false;
FightEvent.instance.enemySprite.overrideSprite=this.GetComponent<SpriteRenderer>().sprite;
}
private IEnumerator Fighting()
{
if (playerTurn == true && enemyTurn == false&&countAtk==true)
{
countAtk = false;
Debug.Log("player");
int damage=attributes.Atk - Def;
if (damage < 0) damage = 0;
HP = HP - damage;
FightEvent.instance.UpdataText(this);
yield return new WaitForSeconds(0.2f);
enemyTurn = true;playerTurn = false; countAtk = true;
}
if (enemyTurn == true && playerTurn == false && countAtk == true)
{
countAtk = false;
Debug.Log("enemyturn");
int damage = Atk - attributes.Def;
if (damage < 0) damage = 0;
attributes.HP = attributes.HP - damage;
FightEvent.instance.UpdataText(this);
yield return new WaitForSeconds(0.2f);
enemyTurn = false; playerTurn = true; countAtk = true;
}
}
}
第一部分中触碰到敌人会触发enemy里Fight()函数,在enemy里他相当于打开一个开关,触发后获取怪物的图片用于显示,并且打开面板,每帧都会启动协程,协程会使我们的角色和敌人轮流“攻击”,并且显示在面板上。扣血计算方式非常简单粗暴。
写完拖到一个怪物身上。start留着以后初始化不同的敌人。
六 面板的显示更新
下来给canvas上建一个脚本,主要用来控制canvas的子物体-战斗面板及该面板上的ui。这个面板应该贯穿始终所以先写了单例并在加载新场景时不摧毁:DontDestroyOnLoad(gameObject)。
public class FightEvent : MonoBehaviour
{
public Transform panel;
public Text heroHP;
public Text heroAtk;
public Text heroDef;
public Image enemySprite;
public Text enemyHP;
public Text enemyAtk;
public Text enemyDef;
private Attributes attributes = null;
public static FightEvent instance;
void Awake()
{
if (instance == null)
instance = this;
else if (instance != this)
Destroy(gameObject);
DontDestroyOnLoad(gameObject);
attributes = GameObject.FindWithTag("Player").GetComponent<Attributes>();
}
public void UpdataText(Enemy enemy)
{
if (!panel.gameObject.activeInHierarchy)
return;
heroHP.text ="生命值"+attributes.HP;
heroAtk.text = "攻击力" + attributes.Atk;
heroDef.text = "防御力" + attributes.Def;
enemyHP.text = "生命值" + enemy.HP;
enemyAtk.text = "攻击力" + enemy.Atk;
enemyDef.text = "防御力" + enemy.Def;
}
public void TogglePanel()
{
panel.gameObject.SetActive(!panel.gameObject.activeSelf);
}
}
后面写了个TogglePanel(),用起来总感觉不放心,没用上。最后一个脚本有好多需要“拖拽”的,别忘了。
有个明显bug就是在触发战斗后,战斗面板打开,角色仍然可以移动。这个是不应该的。
到这第一篇文章就结束了。如果我没弃坑的话,接下来的方向:
1.随机生成关卡及敌人,敌人按tower的层数逐渐变强。
2.背包商店系统,设置一些道具,可能要用到json。
3.锦上添花-角色技能和怪物技能
4.优化-对象池(我现在还没做过对象池,不过知道原理)
---------------------------------------------------------------------------
我写的文章很潦草请见谅。我现在的打算是考研,每周最多只有一天的时间来做个小部分并写一篇文章。我也不知道能坚持多久,但是有您的浏览、点赞就是对我的鼓励。
---------------------------------------------------------------------------
如果你喜欢还可以到我的知乎去看一看:炘辰的知乎主页