【Tower】一、移动和战斗

介绍

这是我在考研期间挖的第二个坑。

二号坑打算做一个【魔塔+roguelike】类型的小游戏。这种类型可以有各种各样的发挥,当然我时间不一定充足,只能先是把基本的部分先造好。

这是这篇文章的效果图:

是不是对画面很无语。身为灵魂画手的我也很无语,但是只是刚开始的demo嘛。

这一篇文章大概就时完成了这样的事:像魔塔里的方式一样移动。碰到怪物了触发战斗。

一 前期准备

如上面的动图先随便准备几张精灵啦,我懒得去unity商店里和我的电脑里找了,我觉得免费的都一般,都是用来建demo的就没啥区别,不影响后面的部分就行。

  1. 把图片拖到unity里,做成精灵图片。别忘了调pixels per unit。
  2. 拖到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.优化-对象池(我现在还没做过对象池,不过知道原理)

---------------------------------------------------------------------------

我写的文章很潦草请见谅。我现在的打算是考研,每周最多只有一天的时间来做个小部分并写一篇文章。我也不知道能坚持多久,但是有您的浏览、点赞就是对我的鼓励。

---------------------------------------------------------------------------

如果你喜欢还可以到我的知乎去看一看:炘辰的知乎主页

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值