平台跳跃游戏《梦之奇旅》代码回顾

目录

1.说明

        1.1项目说明

2.主要控制

        2.1主人公PlayerController

                2.1.1基本行动

                2.1.2生命

                2.1.3分身记录

        2.2虚影分身SubstitudeVController

        2.3实体分身SubstitudeEController

3.相机

        3.1主相机控制MainCameraController

4.场景道具

        4.1一开关对多个门多次触发器SwitchTrigger

        4.2一开关对多个门多次平台SwitchPlatform

        4.3一开关对多个门持续触发器LastingTrigger

        4.4多开关对单一门多次触发器MultiTrigger

        4.5多开关对单一门多次平台MultiPlatform

        4.5伤害区域(刺)Sting

5.UI

        5.1冲刺时间显示UIdashTime

        5.2虚影分身持续时间UIVTime

        5.3实体分身持续时间UITTime

        5.4虚影分身黑幕UIUpBlack, UIDownBlack

6.场景切换

        6.1主菜单MainMenu

        6.2传送门Door

7.动画

        7.1主角动画PlayerAnimationController

        7.2分身动画SubstitudeController

8.声音

        8.1SoundManager

9.总结


1.说明

1.1项目说明

作为一个平台跳跃类游戏,游戏特色为主角拥有控制两个角色行动并解谜的特性。然而,不同于双人游戏的同时控制不同角色,主角可以将自己未来一段时间内想要分身执行的操作“提前规划”,并在想要的时机使用分身。个人认为是十分具有潜力的玩法,可惜是开发时长过短,无法斟酌关卡设计的引导性与游戏性。

2.主要控制

2.1主人公

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.VFX;
using UnityEngine.SceneManagement;

public class PlayerController : MonoBehaviour
{
    //单例化player
    public static PlayerController instance { get; private set; }

    //声明速度
    public float velocity = 0.1f;
    //声明蹲下来时的速度减缓系数
    private float CrouchCoefficient;
    //声明跳跃的力
    public float upforce = 0.1f;
    //声明冲刺的速度
    public float DashSpeed = 1000.0f;
    //声明冲刺内置cd
    public float DashCD = 1.0f;
    float DashTimer;
    bool InDashCD;

    // ----地面位置的坐标,用于检测和地面是否接触,防止之后角色大小导致的raycast中distance改变
    public Transform groundPos;
    // 头顶位置的坐标,用于检测和头顶是否和砖块接触,防止之后角色大小导致的raycast中distance改变
    public Transform ceilingPos;

    //声明刚体组件
    Rigidbody2D rigidbody2d;

    //声明碰撞体组件
    Collider2D BigCollider;
    Collider2D SmallCollider;

    float horizontal;
    bool vertical;
    bool dash;
    bool crouch;

    //是否开始创建V分身
    public static bool StartSubV;
    public float SubVTime = 5.0f;
    float SubVTimer;
    public static Vector2 InitVPosition;

    //是否开始创建E分身
    public static bool StartSubE;//用来记录subE是否触发,持续一次判断
    public float SubETime = 10.0f;
    float SubETimer;
    public static bool StartedSubE;//用来记录subE已经被触发了第一次,持续整个周期


    //声明用来存储替身行为的queue
    public static Queue MoveQueue = new Queue();//float
    public static Queue JumpQueue = new Queue();//bool
    public static Queue CourchQueue = new Queue();//bool
    public static Queue DashQueue = new Queue();//bool
    //bool QueueNotEmpty;


    //挂载两个分身的预制件,一个virtual,一个entity
    public GameObject SubstituteV;
    public GameObject SubstituteE;

    //声明生命
    public int maxHealth = 1;
    int currentHealth;

    //声明动画组件
    Animator animator;
    Vector2 lookdirection = new Vector2();
    Vector2 move;


    public int GetHealth() { return currentHealth; }

    // ------------------------------------Start is called before the first frame update
    void Start()
    {
        //PlayerController已初始化
        rigidbody2d = GetComponent<Rigidbody2D>();
        BigCollider = GetComponent<BoxCollider2D>();
        SmallCollider = GetComponent<CapsuleCollider2D>();
        SmallCollider.enabled = false;

        //初始化dash的计时器
        DashTimer = DashCD;

        //初始化分身计时器
        SubETimer = 0;
        SubVTimer = 0;

        //初始化分身判定,因为最开始没有叫分身
        StartSubV = false; StartSubE = false; StartedSubE = false;

        //初始化生命
        currentHealth= maxHealth;

        //获取动画组件
        animator= GetComponent<Animator>();
    }

    // --------------------------------------Update is called once per frame
    void Update()
    {
        //QueueNotEmpty = MoveQueue.Count>0 && JumpQueue.Count>0 && CourchQueue.Count>0 && DashQueue.Count>0;
        //判断,此时如果没有创建V分身,则正常运行
        if (!StartSubV)
        {
            StartSubV = Input.GetKey(KeyCode.J);
            //if ( !StartSubE )
            //{
            StartSubE = Input.GetKeyDown(KeyCode.I);

            //}


            horizontal = Input.GetAxis("Horizontal");
            vertical = Input.GetKey(KeyCode.K);
            dash = Input.GetKey(KeyCode.L);
            crouch = Input.GetKey(KeyCode.S);

            if (InDashCD)
            {
                DashTimer -= Time.deltaTime;
                //Debug.Log("进入冲刺冷却时间");
                if (DashTimer < 0)
                {
                    InDashCD = false;
                }
                //Debug.Log("Dashtimeer变量上传给ui");
                UIdashTime.instance.SetdashTime(DashTimer / (float)DashCD);
            }

            //若subV被触发
            if (StartSubV && !StartSubE)//只有在subv启动,且sube没启动的时候能触发subv
            {
                //重置动作,直接静止
                horizontal = 0;
                vertical = false;
                dash = false;
                crouch = false;

                //拉起黑幕UIup,UIdown
                UIDownBlack.instance.Activate();
                UIUpBlack.instance.Activate();

                //重置计时器
                SubVTimer = SubVTime;
                //实例化V分身,命名为Subv。生成位置为面朝方向的2.0f位置----------------------------------2.0f的数值可以改
                //Debug.Log("分身V被创造");
                GameObject SubV = Instantiate(SubstituteV,rigidbody2d.position + Vector2.right * horizontal * 3.0f, Quaternion.identity);
      
            }



            if (StartSubE)
            {
                StartedSubE = true;
                //重置计时器
                SubETimer = SubETime;
                //实例化E分身,命名为Sube。生成位置为面朝方向的2.0f位置----------------------------------2.0f的数值可以改
                //Debug.Log("分身E被创造");
                GameObject SubE = Instantiate(SubstituteE, InitVPosition, Quaternion.identity);
            }

            //动画相关变量获取
            move = new Vector2(horizontal, 0.0f);
            if (!Mathf.Approximately(move.x,0.0f))
            {
                lookdirection.Set(move.x, 0.0f);
                lookdirection.Normalize();
            }
            animator.SetFloat("MoveX", lookdirection.x);
            animator.SetFloat("Speed", move.magnitude);


        }

        //此时,创建了V分身,进行倒计时
        else
        {
            SubVTimer -= Time.deltaTime;
            if (SubVTimer < 0)
            {
                StartSubV = false;
                //降下黑幕
                UIDownBlack.instance.Deactivate();
                UIUpBlack.instance.Deactivate();
            }
            else
            {
                UIVTime.instance.SetVTime(SubVTimer/(float)SubVTime);
            }
        }




    }

    //------------------------------------------FixedUpdate
    private void FixedUpdate()
    {
        //检查生命值,若生命值小于等于0,则回到mainscene
        HealthCheck();


        //Debug.Log("SubE is discounting");
        SubETimer -= Time.deltaTime;
        if (SubETimer < 0)
        {
            StartSubE = false;
            StartedSubE = false;
        }
        else
        {
            UIETime.instance.SetETime(SubETimer / (float)SubETime);

        }






        //先水平移动
        PlayerMoveHorizontal();

        //在判断是否蹲下
        PlayerCrouch(crouch);


        //判断是否冲刺,如果在蹲,则不能冲刺----------有个问题,空中蹲不能冲刺
        if (dash && !crouch)
        {
            PlayerDash();

        }


        //射出一条射线,判断此时是否在地面或者在分身上,长度为1.0f。-----------后续会修改
        //Debug.Log("踩踏判断:");
        //Debug.Log(hitGround.collider != null);
        //Debug.Log(hitPlayer.collider != null);

            //判断跳跃
        if (vertical)
        {
            //Debug.Log("有跳跃输入");
            //RaycastHit2D hitGround = Physics2D.Raycast(rigidbody2d.position, Vector2.down, 0.9f, LayerMask.GetMask("Ground"));
            //RaycastHit2D hitPlayer = Physics2D.Raycast(rigidbody2d.position + Vector2.down * 0.9f, Vector2.down, 0.0f, LayerMask.GetMask("Player"));
            //RaycastHit2D hitShadow = Physics2D.Raycast(rigidbody2d.position + Vector2.down * 0.9f, Vector2.down, 0.0f, LayerMask.GetMask("Shadow"));

            //RaycastHit2D hitGround = Physics2D.Raycast(groundPos.position, Vector2.down, 0.1f, LayerMask.GetMask("Ground"));
            //RaycastHit2D hitPlayer = Physics2D.Raycast(groundPos.position +Vector3.down*0.1f, Vector2.down, 0.01f, LayerMask.GetMask("Player"));
            //RaycastHit2D hitShadow = Physics2D.Raycast(groundPos.position +Vector3.down*0.1f, Vector2.down, 0.01f, LayerMask.GetMask("Shadow"));
            Debug.Log("踩踏判断:");
            Debug.Log(hitGround.collider != null);
            Debug.Log(hitPlayer.collider != null);
            //if (hitGround.collider != null || hitPlayer.collider != null || hitShadow.collider != null)
            //{
            //    //Debug.Log("有跳跃");
            //    PlayerJump();

            //}
            Collider2D[] hits = Physics2D.OverlapCircleAll(groundPos.position, 0.5f);
            foreach (Collider2D hit in hits)
            {
                //Debug.Log(hit.name);
                //Debug.Log(hit.tag);
                //Debug.Log("-");
                if (hit == this.BigCollider) continue;

                if (hit.CompareTag("Ground") || hit.CompareTag("Player"))
                    //Debug.Log("已跳跃");
                    PlayerJump();
            }

        }
    }



    //--------------------------------------------Movement
    //根据数据水平移动
    private void PlayerMoveHorizontal()
    {
        Vector2 position = transform.position;
        position.x += velocity * horizontal * Time.deltaTime * CrouchCoefficient;
        rigidbody2d.position = position;

    }


    //根据指令,跳跃
    private void PlayerJump()
    {
        //施加一个向上的力
        rigidbody2d.AddForce(Vector2.up * upforce);
        animator.SetTrigger("Jump");

    }


    //根据数据,如果不在cd内,冲刺,如果在cd内,return
    private void PlayerDash()
    {
        if (InDashCD)
        {
            return;
        }
        else
        {
            rigidbody2d.AddForce(Vector2.right * horizontal * DashSpeed);
            DashTimer = DashCD;
            InDashCD = true;
            animator.SetTrigger("Dash");

        }
    }


    //根据输入,如果一直按键蹲下,则取消大碰撞体判断,如果不蹲下,且头上没东西,则重新恢复大碰撞体
    private void PlayerCrouch(bool judge)
    {
        if (judge)
        {
            CrouchCoefficient = 0.5f;
            BigCollider.enabled = false;
            SmallCollider.enabled = true;
        }

        else
        {
            //对蹲下动作进行检测!!!!
            //判断头顶有没有碰撞到物体,否则不起来了----------0的大小后续会修改
            RaycastHit2D hitHead = Physics2D.Raycast(rigidbody2d.position, Vector2.up, 0.2f, LayerMask.GetMask("Ground"));
            if (hitHead.collider == null)
            {
                CrouchCoefficient = 1.0f;
                BigCollider.enabled = true;
                SmallCollider.enabled = false;
            }

        }


    }
    //-----------------------------------------------Judgement


    public void ChangeHealth(int amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
    }
    void HealthCheck()
    {
        if (currentHealth <= 0)
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        }
    }
}

 主角作为唯一角色,可使用单例,使得类中参数可直接被调用。

public static PlayerController instance { get; private set; }

由于分身是作为预制件被实例化,无法保存信息,所以关于virtual分身记录的玩家行为,将作为FixedUpdate()里面的信息,被记录入四个不同的queue,分别对应左右行动、跳、蹲、冲刺。

    //声明用来存储替身行为的queue
    public static Queue MoveQueue = new Queue();//float
    public static Queue JumpQueue = new Queue();//bool
    public static Queue CourchQueue = new Queue();//bool
    public static Queue DashQueue = new Queue();//bool

2.1.1基本行动

左右行动,获取getAxis("Horizontal"),并更改对应刚体位置,详细见ruby advanture项目学习。

蹲,通过设置两个collider,一个box一个capsule,一大一小。并在初始化中将小的enable = false,并转化二者enable,做到在需要的时候使用不同collider。并且蹲下时有速度削减,系数为CrouchCoefficient,系数默认为1.0f,减免为0.5f,体现在位移上,则乘以CrouchCoefficient。同时,从蹲下恢复到站立,则需要另加对头顶距离的判断,若头顶没有碰到东西,才可以恢复站立。

    //根据输入,如果一直按键蹲下,则取消大碰撞体判断,如果不蹲下,且头上没东西,则重新恢复大碰撞体
    private void PlayerCrouch(bool judge)
    {
        if (judge)
        {
            CrouchCoefficient = 0.5f;
            BigCollider.enabled = false;
            SmallCollider.enabled = true;
        }

        else
        {
            //对蹲下动作进行检测!!!!
            //判断头顶有没有碰撞到物体,否则不起来了----------0的大小后续会修改
            RaycastHit2D hitHead = Physics2D.Raycast(rigidbody2d.position, Vector2.up, 0.2f, LayerMask.GetMask("Ground"));
            if (hitHead.collider == null)
            {
                CrouchCoefficient = 1.0f;
                BigCollider.enabled = true;
                SmallCollider.enabled = false;
            }

        }
    }

跳,向上的addforce,与冲刺类似,后者为横向的addforce。同时,为了防止跳作为“getkey()”在update里被不断触发,需要设置条件语句

            //判断跳跃
        if (vertical)
        {
            //Debug.Log("有跳跃输入");
            //RaycastHit2D hitGround = Physics2D.Raycast(rigidbody2d.position, Vector2.down, 0.9f, LayerMask.GetMask("Ground"));
            //RaycastHit2D hitPlayer = Physics2D.Raycast(rigidbody2d.position + Vector2.down * 0.9f, Vector2.down, 0.0f, LayerMask.GetMask("Player"));
            //RaycastHit2D hitShadow = Physics2D.Raycast(rigidbody2d.position + Vector2.down * 0.9f, Vector2.down, 0.0f, LayerMask.GetMask("Shadow"));

            //RaycastHit2D hitGround = Physics2D.Raycast(groundPos.position, Vector2.down, 0.1f, LayerMask.GetMask("Ground"));
            //RaycastHit2D hitPlayer = Physics2D.Raycast(groundPos.position +Vector3.down*0.1f, Vector2.down, 0.01f, LayerMask.GetMask("Player"));
            //RaycastHit2D hitShadow = Physics2D.Raycast(groundPos.position +Vector3.down*0.1f, Vector2.down, 0.01f, LayerMask.GetMask("Shadow"));
            Debug.Log("踩踏判断:");
            Debug.Log(hitGround.collider != null);
            Debug.Log(hitPlayer.collider != null);
            //if (hitGround.collider != null || hitPlayer.collider != null || hitShadow.collider != null)
            //{
            //    //Debug.Log("有跳跃");
            //    PlayerJump();

            //}
            Collider2D[] hits = Physics2D.OverlapCircleAll(groundPos.position, 0.5f);
            foreach (Collider2D hit in hits)
            {
                //Debug.Log(hit.name);
                //Debug.Log(hit.tag);
                //Debug.Log("-");
                if (hit == this.BigCollider) continue;

                if (hit.CompareTag("Ground") || hit.CompareTag("Player"))
                    //Debug.Log("已跳跃");
                    PlayerJump();
            }

        }

两种办法原理类似,都是判断脚底空间是tag为Ground或者Player才能跳,如果什么都没有就是null,无法成立。

Attention:tag必须要在每个对象中单独更改!仅改变父对象tag并不会影响子对象。如tilemap中改变grid的tag并不会将tag应用至具体的地面或者其他地形,必须在子对象中单独更改。

与之类似的冲刺,因为也是用的getkey(),所以干脆加入了冲刺cd的设定。具体细节类似ruby项目中的无敌时间设计,声明中的DashCD, DashTimer, InDashCD三个变量组成计时器判断。同时,由于蹲下的时候不能冲刺,所以有判断if (dash && !crouch)。

至此,人物的基本运动就设计完了。基本运动还被用于V分身和E分身的运动,提到分身代码时会略过。

2.1.2生命

这部分做的比较粗糙,与ruby项目中生命相仿,有ChangeHealth(int amount)函数。不过增加HealthCheck()函数,放在FixedUpdate()中,若生命小于等于0,则重新开始当前关卡,即重新加载当前Scene。

    void HealthCheck()
    {
        if (currentHealth <= 0)
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        }
    }

2.1.3分身记录

分身记录主要体现在Update()中。

StartSubV,StartSubE分别为确认分身是否存在的bool。因为只有在V分身出现的时候才不取用户输入,所以最开始为判断V分身的条件语句,如果V分身出现的话,进行V分身的倒计时,并在结束倒计时时将StartSubV改为false。

因为分身V只出现一次,所以仅在第一次接收到“J”输入的时候实例化分身V,并且在之后倒计时结束前不进入此判断。

Attention:StartSubV,StartSubE为检测当前场上情况的重要标准,被用来判断相机的跟踪目标;分身V、分身E是否摧毁自身等条件判断。

2.2虚影分身

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SubstitudeVController : MonoBehaviour
{
    //声明速度
    public float velocity = 0.1f;
    //声明蹲下来时的速度减缓系数
    private float CrouchCoefficient;
    //声明跳跃的力
    public float upforce = 0.1f;
    //声明冲刺的速度
    public float DashSpeed = 1000.0f;
    //声明冲刺内置cd
    public float DashCD = 1.0f;
    float DashTimer;
    bool InDashCD;
    // 地面位置的坐标,用于检测和地面是否接触,防止之后角色大小导致的raycast中distance改变
    public Transform groundPos;
    // 头顶位置的坐标,用于检测和头顶是否和砖块接触,防止之后角色大小导致的raycast中distance改变
    public Transform ceilingPos;

    //声明刚体组件
    Rigidbody2D rigidbody2d;

    //声明碰撞体组件
    Collider2D BigCollider;
    Collider2D SmallCollider;

    float horizontal;
    bool vertical;
    bool dash;
    bool crouch;


    public GameObject Player;

    //声明动画组件
    Animator animator;
    Vector2 lookdirection = new Vector2();
    Vector2 move;

    // ------------------------------------Start is called before the first frame update
    void Start()
    {
        //Debug.Log("SubV已初始化");
        //ModifiedPlayerController已初始化
        rigidbody2d = GetComponent<Rigidbody2D>();
        BigCollider = GetComponent<BoxCollider2D>();
        SmallCollider = GetComponent<CapsuleCollider2D>();
        SmallCollider.enabled = false;
        //初始化dash的计时器
        DashTimer = DashCD;

        Player.GetComponent<ModifiedPlayerController>();

        horizontal = 0;
        vertical = false;
        dash = false;
        crouch= false;

        PlayerController.MoveQueue.Clear();
        PlayerController.JumpQueue.Clear();
        PlayerController.DashQueue.Clear();
        PlayerController.CourchQueue.Clear();

        //初始化位置
        //Debug.Log("InitVTransform has been assigned");
        PlayerController.InitVPosition = rigidbody2d.transform.position;

        animator= GetComponent<Animator>();
    }

    // --------------------------------------Update is called once per frame
    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetKey(KeyCode.K);
        dash = Input.GetKey(KeyCode.L);
        crouch = Input.GetKey(KeyCode.S);


        if (InDashCD)
        {
            DashTimer -= Time.deltaTime;
            //Debug.Log("进入冲刺冷却时间");
            if (DashTimer < 0)
            {
                InDashCD = false;
            }
            UIdashTime.instance.SetdashTime(DashTimer / (float)DashCD);
        }

        if (!PlayerController.StartSubV)
        {
            Destroy(gameObject);
        }

        //maincamera的视角正在跟着当前位置移动
        //MainCamera.transform.position = transform.position;

    }

    //------------------------------------------FixedUpdate
    private void FixedUpdate()
    {
        //先水平移动
        PlayerMoveHorizontal();
        PlayerController.MoveQueue.Enqueue(horizontal);//每个行为后都将行为记录
        //Debug.Log("行为已记录");

        //在判断是否蹲下
        PlayerCrouch(crouch);
        PlayerController.CourchQueue.Enqueue(crouch);//记录行为

        //判断是否冲刺,如果在蹲,则不能冲刺----------有个问题,空中蹲不能冲刺
        if (dash && !crouch)
        {
            PlayerDash();
        }
        PlayerController.DashQueue.Enqueue(dash);//记录行为


        //判断跳跃
        if (vertical)
        {   

            Debug.Log("有跳跃输入");
            //RaycastHit2D hitGround = Physics2D.Raycast(groundPos.position, Vector2.down, 0.1f, LayerMask.GetMask("Ground"));
            //RaycastHit2D hitPlayer = Physics2D.Raycast(groundPos.position + Vector3.down * 0.1f, Vector2.down, 0.01f, LayerMask.GetMask("Player"));
            //RaycastHit2D hitShadow = Physics2D.Raycast(groundPos.position + Vector3.down * 0.1f, Vector2.down, 0.01f, LayerMask.GetMask("Shadow"));
            Debug.Log(hitGround.collider != null);
            Debug.Log(hitPlayer.collider!= null);

            Collider2D []hits = Physics2D.OverlapCircleAll(groundPos.position, 0.5f);
            foreach (Collider2D hit in hits)
            {
                //Debug.Log(hit.name);
                //Debug.Log(hit.tag);
                //Debug.Log("-");
                if (hit == this.BigCollider) continue;

                if (hit.CompareTag("Ground") || hit.CompareTag("Player"))
                    //Debug.Log("已跳跃");
                    PlayerJump();
            }
        }
        PlayerController.JumpQueue.Enqueue(vertical);//记录行为
        
        //动画相关变量获取
        move = new Vector2(horizontal, 0.0f);
        if (!Mathf.Approximately(move.x, 0.0f))
        {
            lookdirection.Set(move.x, 0.0f);
            lookdirection.Normalize();
        }
        animator.SetFloat("MoveX", lookdirection.x);
        animator.SetFloat("Speed", move.magnitude);
    }
    //--------------------------------------------Movement
    //根据数据水平移动
    private void PlayerMoveHorizontal()
    {
        Vector2 position = transform.position;
        position.x += velocity * horizontal * Time.deltaTime * CrouchCoefficient;
        rigidbody2d.position = position;
    }


    //根据指令,跳跃
    private void PlayerJump()
    {
        //施加一个向上的力
        rigidbody2d.AddForce(Vector2.up * upforce);
        animator.SetTrigger("Jump");

    }

    //根据数据,如果不在cd内,冲刺,如果在cd内,return
    private void PlayerDash()
    {
        if (InDashCD)
        {
            return;
        }
        else
        {
            rigidbody2d.AddForce(Vector2.right * horizontal * DashSpeed);
            DashTimer = DashCD;
            InDashCD = true;
            animator.SetTrigger("Dash");
        }
    }
    //根据输入,如果一直按键蹲下,则取消大碰撞体判断,如果不蹲下,且头上没东西,则重新恢复大碰撞体
    private void PlayerCrouch(bool judge)
    {
        if (judge)
        {
            CrouchCoefficient = 0.5f;
            BigCollider.enabled = false;
            SmallCollider.enabled = true;
        }

        else
        {
            //对蹲下动作进行检测!!!!
            //判断头顶有没有碰撞到物体,否则不起来了----------0的大小后续会修改
            RaycastHit2D hitHead = Physics2D.Raycast(rigidbody2d.position, Vector2.up, 0.2f, LayerMask.GetMask("Ground"));
            if (hitHead.collider == null)
            {
                CrouchCoefficient = 1.0f;
                BigCollider.enabled = true;
                SmallCollider.enabled = false;
            }
        }

    }
}

虚影分身被创建于主角按下“J” 的时刻,并且负责记录玩家输入。首先,为了让玩家每次召唤V分身时都重置E分身的规划路线,所以需要Clear()所有queue。并且保证记录的信息不受电脑性能的影响,所以Enqueue()的信息放在FixedUpdate()中。一旦PlayerController.StartSubV = false,意味着V分身的时间到了,Destroy(gameObject)。

2.3实体分身

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SubstitudeEController : MonoBehaviour
{
    //声明速度
    public float velocity = 0.1f;
    //声明蹲下来时的速度减缓系数
    private float CrouchCoefficient;
    //声明跳跃的力
    public float upforce = 0.1f;
    //声明冲刺的速度
    public float DashSpeed = 1000.0f;
    //声明冲刺内置cd
    public float DashCD = 1.0f;
    float DashTimer;
    bool InDashCD;
    // 地面位置的坐标,用于检测和地面是否接触,防止之后角色大小导致的raycast中distance改变
    public Transform groundPos;
    // 头顶位置的坐标,用于检测和头顶是否和砖块接触,防止之后角色大小导致的raycast中distance改变
    public Transform ceilingPos;

    //声明刚体组件
    Rigidbody2D rigidbody2d;

    //声明碰撞体组件
    Collider2D BigCollider;
    Collider2D SmallCollider;

    float horizontal;
    bool vertical;
    bool dash;
    bool crouch;


    public GameObject Player;

    //声明动画组件
    Animator animator;
    Vector2 lookdirection = new Vector2();
    Vector2 move;

    // ------------------------------------Start is called before the first frame update
    void Start()
    {
        //PlayerController已初始化
        rigidbody2d = GetComponent<Rigidbody2D>();
        BigCollider = GetComponent<BoxCollider2D>();
        SmallCollider = GetComponent<CapsuleCollider2D>();
        SmallCollider.enabled = false;
        //初始化dash的计时器
        DashTimer = DashCD;

        Player.GetComponent<PlayerController>();

        
        animator= GetComponent<Animator>(); 

    }

    // --------------------------------------Update is called once per frame
    void Update()
    {
    }

    //------------------------------------------FixedUpdate
    private void FixedUpdate()
    {
        if (PlayerController.MoveQueue.Count>0)
        {
            //Debug.Log($"MoveQueue's count is : {PlayerController.MoveQueue.Count}");
            horizontal = (float)PlayerController.MoveQueue.Dequeue();
            //Debug.Log($"JumpQueue's count is : {PlayerController.JumpQueue.Count}");
            vertical = (bool)PlayerController.JumpQueue.Dequeue();
            //Debug.Log($"DashQueue's count is : {PlayerController.DashQueue.Count}");
            dash = (bool)PlayerController.DashQueue.Dequeue();
            //Debug.Log($"CourchQueue's count is : {PlayerController.CourchQueue.Count}");
            crouch = (bool)PlayerController.CourchQueue.Dequeue();
            if (InDashCD)
            {
                DashTimer -= Time.deltaTime;
                //Debug.Log("进入冲刺冷却时间");
                if (DashTimer < 0)
                {
                    InDashCD = false;
                }
            }

            //先水平移动
            PlayerMoveHorizontal();

            //在判断是否蹲下
            PlayerCrouch(crouch);

            //判断是否冲刺,如果在蹲,则不能冲刺----------有个问题,空中蹲不能冲刺
            if (dash && !crouch)
            {
                PlayerDash();

            }

            //判断跳跃
            if (vertical)
            {
                //RaycastHit2D hitGround = Physics2D.Raycast(groundPos.position, Vector2.down, 0.1f, LayerMask.GetMask("Ground"));
                //RaycastHit2D hitPlayer = Physics2D.Raycast(groundPos.position + Vector3.down * 0.9f, Vector2.down, 0.1f, LayerMask.GetMask("Player"));
                //RaycastHit2D hitShadow = Physics2D.Raycast(groundPos.position + Vector3.down * 0.9f, Vector2.down, 0.1f, LayerMask.GetMask("Shadow"));

                //if (hitGround.collider != null || hitPlayer.collider != null || hitShadow.collider != null)
                //{
                //    PlayerJump();


                //    //Debug.Log($"MoveQueue dequeue:{MoveQueue.Dequeue()}");
                //}

                Collider2D[] hits = Physics2D.OverlapCircleAll(groundPos.position, 0.5f);
                foreach (Collider2D hit in hits)
                {
                    //Debug.Log(hit.name);
                    //Debug.Log(hit.tag);
                    //Debug.Log("-");
                    if (hit == this.BigCollider) continue;
                    if (hit.CompareTag("Ground") || hit.CompareTag("Player"))
                        //Debug.Log("已跳跃");
                        PlayerJump();
                }

            }
        }
        else if (!PlayerController.StartedSubE)
        {
            Destroy(gameObject);
        }
        else
        {
            //Debug.Log($"MoveQueue:{PlayerController.MoveQueue.Count}");
            horizontal = 0;
            vertical = false;
            dash = false;
            crouch= false;
            Destroy(rigidbody2d);
        }
        //动画相关变量获取
        move = new Vector2(horizontal, 0.0f);
        if (!Mathf.Approximately(move.x, 0.0f))
        {
            lookdirection.Set(move.x, 0.0f);
            lookdirection.Normalize();
        }
        animator.SetFloat("MoveX", lookdirection.x);
        animator.SetFloat("Speed", move.magnitude);
    }
    //--------------------------------------------Movement
    //根据数据水平移动
    private void PlayerMoveHorizontal()
    {
        Vector2 position = transform.position;
        position.x += velocity * horizontal * Time.deltaTime * CrouchCoefficient;
        rigidbody2d.position = position;
    }

    //根据指令,跳跃
    private void PlayerJump()
    {
        //施加一个向上的力
        rigidbody2d.AddForce(Vector2.up * upforce);
        animator.SetTrigger("Jump");
    }


    //根据数据,如果不在cd内,冲刺,如果在cd内,return
    private void PlayerDash()
    {
        if (InDashCD)
        {
            return;
        }
        else
        {
            rigidbody2d.AddForce(Vector2.right * horizontal * DashSpeed);
            DashTimer = DashCD;
            InDashCD = true;
            animator.SetTrigger("Dash");
        }
    }

    //根据输入,如果一直按键蹲下,则取消大碰撞体判断,如果不蹲下,且头上没东西,则重新恢复大碰撞体
    private void PlayerCrouch(bool judge)
    {
        if (judge)
        {
            CrouchCoefficient = 0.5f;
            BigCollider.enabled = false;
            SmallCollider.enabled = true;
        }

        else
        {
            //对蹲下动作进行检测!!!!
            //判断头顶有没有碰撞到物体,否则不起来了----------0的大小后续会修改
            RaycastHit2D hitHead = Physics2D.Raycast(rigidbody2d.position, Vector2.up, 0.2f, LayerMask.GetMask("Ground"));
            if (hitHead.collider == null)
            {
                CrouchCoefficient = 1.0f;
                BigCollider.enabled = true;
                SmallCollider.enabled = false;
            }
        }
    }
}

实体分身由于是完全复刻V分身的信息,V分身的信息是在FixedUpdate()中被记录,因此E分身的信息Dequeue()也放在FixedUpdate()当中。StartSubE和StaredSubE两个变量,一个是记录E分身被触发直到执行完动作,一个是记录E分身从触发直到被摧毁。因为E分身的持续时间比V分身久,并且会在执行完V分身的最后指令后(也就是queue.count() == 0)在原地固定,也就是各个动作指令为默认,且刚体组件的碰撞取消。最后在StartedSubE结束后,进行摧毁。

3.相机

3.1主相机控制

using Cinemachine;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;



public class MainCameraController : MonoBehaviour
{
    public float followSpeed = 2.0f;
    public float minX;
    public float maxX;
    public float minY;
    public float maxY;
    private Transform PlayerTransform;
    private Vector2 targetPosition;

    //挂接想要的摄像机中心
    public GameObject Player;
    GameObject SubstitudeV;
    bool LetFindV = true;
    //GameObject SubstitudeE;
    //bool LetFindE = true;

    void Start()
    {
        Player.GetComponent<PlayerController>();

        PlayerTransform = Player.transform;
        transform.position = PlayerTransform.position;

    }


    private void Update()
    {
    }

    private void LateUpdate()
    {
        if (!PlayerController.StartSubV)
        {
            PlayerTransform = Player.transform;
            LetFindV = true;
            //Debug.Log("根据Player决定Camera位置");
        }

        else if (PlayerController.StartSubV)
        {
            if (LetFindV)//为了优化查找过程
            {
                SubstitudeV = GameObject.Find("SubstitudeV(Clone)");
                //if (SubstitudeV == null)
                //{
                //    Debug.Log("找不到SubstitudeV(Clone)");
                //}
                SubstitudeV.GetComponent<SubstitudeVController>();
                LetFindV = false;
            }
            else if (SubstitudeV == null)
            {
                PlayerTransform = Player.transform;

            }
            else
            {
                PlayerTransform = SubstitudeV.transform;
            }
            //Debug.Log("根据SubV决定Camera位置");
        }

        transform.position = PlayerTransform.position;


        if (PlayerTransform != null)
        {
            //Debug.Log("相机正在跟踪");
            targetPosition.x = Mathf.Clamp(PlayerTransform.position.x, minX, maxX);
            targetPosition.y = Mathf.Clamp(PlayerTransform.position.y, minY, maxY);
            Vector2 v1 = Vector2.Lerp(transform.position, targetPosition, followSpeed * Time.deltaTime);
            transform.position = new Vector3(v1.x, v1.y, -10);
        }
    }

}

相机在此的职责是一直跟随主角Player,直到使用V分身。此时跟随V分身行动,直到V分身时间到,并将相机重新跟随回主角。同时,相机的坐标有上限,不能超过一定范围,表现为minX, maxX, minY, maxY。

相机挂接Player的组件,这样可以随时访问主角位置。同时在有需求的时候(StartSubV == true)在Hierarchy中通过名字的方式寻找V分身的组件,并且获得组件相关位置信息。

Attention:GameObject.Find("??")这个方法非常消耗资源,不能在Update()中高频使用,也最好别在LateUpdate()或者FixedUpdate()中无条件地不断查找。所以最好的办法是添加LetFindV这样的条件判断,判断是否这个时候应该寻找V的名称(在本项目中为SubstitudeV(Clone))。并且,游戏组件只需要查找一次获取即可,像指针一样。多次重复地获取或者查找都是无意义的。

4.场景道具

4.1一开关对多个门多次触发器SwitchTrigger

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SwitchTrigger : MonoBehaviour
{
    public SwitchPlatform[] PlatformSet;

    private void OnTriggerEnter2D(Collider2D other)
    {
        
        if (other.gameObject.layer == 6)
        {          
            OperateSet(PlatformSet);
            //Destroy(gameObject);
        }  
    }

    private void OperateSet(SwitchPlatform[] set)
    {
        for (int j = 0; j < set.Length; j++)
        {
            SwitchPlatform st = set[j].GetComponent<SwitchPlatform>();
            st.Switch();
        }
    }
}

声明类名为SwitchPlatform的组,用来挂接多个用来控制的门。

若有物体进入触发器,且layer为6,如下图。layer6为Player标签,即玩家与触发器碰撞,则调用OperateSet()方法。

其中,OperateSet()导入平台组,使用遍历,分别获得其组件,并且调用Switch()办法。

4.2一开关对多个门多次平台SwitchPlatform

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SwitchPlatform : MonoBehaviour
{
    //public GameObject Trigger;
    public bool StartExist = true;
    bool Exist;

    // Start is called before the first frame update
    void Start()
    {
        Exist = StartExist;
        gameObject.SetActive(StartExist);
    }

    public void Switch()
    {
        //Debug.Log("平台收到指令");
        Exist = !Exist;
        //gameObject.transform.localScale = Vector3.zero;
        gameObject.SetActive(Exist);
    }

声明公开的StartExist,用来区别一开始存在的门,以及一开始不存在的门。

Exist用来判断当前门是否存在。

一旦使用公开函数Switch(),Exist状态切换,并且将游戏组件setActive()变为Exist状态。

4.3一开关对多个门持续触发器LastingTrigger

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LastingTrigger : MonoBehaviour
{
    public SwitchPlatform[] PlatformSet;

    public float LastingTime = 3.0f;
    float timer = 0.0f;
    bool isLasting = false;

    void FixedUpdate()
    {
        if (isLasting)
        {
            timer -= Time.deltaTime;
            if (timer < 0)
            {
                OperateSet(PlatformSet);
                isLasting= false;
            }
        }
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.layer == 6)
        {
            OperateSet(PlatformSet);
            timer = LastingTime;
            isLasting = true;
        }

    }

    private void OperateSet(SwitchPlatform[] set)
    {
        for (int j = 0; j < set.Length; j++)
        {
            set[j].GetComponent<SwitchPlatform>().Switch();
        }
    }
}

与4.1类似,但是门具有在一定时间后恢复的特性。这里从Trigger入手更改,而不从Platform部分更改,使得开关具有在一定时间内无法通过重复按,从而延长门的持续时间的特性。重复按开关,只会使得门状态不断改变,计时器不断重置。可以根据需求更改。

与其他计时器类似,具有三个新的计数属性。并且在layer为Player碰撞触发器时,首先调用OperateSet(),其次启动计时器。

计时器第一次结束时,再次调用一次OperateSet()函数。从而达成门在一段时间后恢复的特性。

4.4多开关对单一门多次触发器MultiTrigger

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MultiTrigger : MonoBehaviour
{
    bool trigger = false;
    private void OnTriggerStay2D(Collider2D other)
    {

        if (other.gameObject.layer == 6)
        {
            trigger= true;
        }

    }

    public bool GetTrigger()
    {
        return trigger;
    }
}

由于多开关对单一门的启动特性,开关代码基本只需要评判开关状态即可。主要逻辑在MultiPlatform中。

4.5多开关对单一门多次平台MultiPlatform

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MultiPlatform : MonoBehaviour
{
    public bool StartExist = true;
    bool Exist;

    public MultiTrigger[] MultiTriggerSet;

    Queue<bool> Judge = new Queue<bool>();

    void Start()
    {
        Judge.Clear();
        Exist = StartExist;
    }

    private void FixedUpdate()
    {
        gameObject.SetActive(Exist);

        Judge.Clear() ;
        for (int i = 0; i < MultiTriggerSet.Length; i++)
        {
            Judge.Enqueue(MultiTriggerSet[i].GetComponent<MultiTrigger>().GetTrigger());        
        }

        if (Judge.Contains(false))
        {
            Exist = true;
        }
        else
        {
            Exist = false;
        }
    }
}

与4.2StartExist用途类似,设计不同起始状态。

创建专门用来判断开关状态的Queue。在FixedUpdate()中不断进行Judge状态的更新,并且判断其中是否含有没有开的开关状态。

Attention:这边由于触发器组件在最开始就挂接好了,所以GetComponent部分可以放在Start()中,用别的列承接组件信息,以免不停地GetComponent,消耗资源。

4.5伤害区域(刺)Sting

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Sting : MonoBehaviour
{
    public int Damage = -1;

    private void OnTriggerEnter2D(Collider2D other)
    {
        PlayerController playercontroller = other.GetComponent<PlayerController>();

        if (playercontroller != null)
        {
            if (playercontroller.GetHealth() > 0)
            {
                playercontroller.ChangeHealth(Damage);
            }
        }
    }
}

此部分类似Ruby项目中的DamagableZone,声明生命更改量。一旦进入触发器,则调用Player的ChangeHealth()方法。

5.UI

5.1冲刺时间显示UIdashTime

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIdashTime : MonoBehaviour
{
    //创建公有静态成员,获取当前对象本身
    public static UIdashTime instance { get; private set; }
    public Image mask;
    float originalSize;

    // Start is called before the first frame update
    void Awake()
    {
        instance = this;
    }

    private void Start()
    {
        //获取遮罩层图像初始宽度
        originalSize = mask.rectTransform.rect.width;
    }

    public void SetdashTime(float percentage)
    {
        mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originalSize * percentage);
    }
}

UI的设置在Canvas里面分为三个部分,第一部分surround是UI的底层图片,第二部分mask为需要控制的部分,第三部分image则为mask上所需的贴图。UI的控制是通过控制mask的Size来掌握的。

单例化UIdashTime也是为了让各个其他组件都可以轻易访问,不用再去在每个Scene中重新挂接UI组件。

以下UIVTime和UIETime也是相同结构,不过在Player中的不同位置被call,且传入数值不同。

5.2虚影分身持续时间UIVTime

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIVTime : MonoBehaviour
{
    public static UIVTime instance { get; private set; }
    public Image mask;
    float originalSize;

    void Awake()
    {
        instance= this;
    }

    private void Start()
    {
        originalSize = mask.rectTransform.rect.width;
    }

    public void SetVTime(float percentage)
    {
        mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal,originalSize * percentage);
    }
}

5.3实体分身持续时间UITTime

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIETime : MonoBehaviour
{
    public static UIETime instance { get; private set; }
    public Image mask;
    float originalSize;

    void Awake()
    {
        instance = this;
    }

    private void Start()
    {
        originalSize = mask.rectTransform.rect.height;
    }

    public void SetETime(float percentage)
    {
        mask.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, originalSize * percentage);
    }
}

5.4虚影分身黑幕UIUpBlack, UIDownBlack

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIUpBlack : MonoBehaviour
{

    public static UIUpBlack instance { get; private set; }
    public Image mask;
    Vector3 initPosition;
    public float distance = 174.0f;
    Vector3 destination;

    void Awake()
    {
        instance = this;
    }

    private void Start()
    {
        initPosition = this.transform.position;
        destination = new Vector3(transform.position.x, transform.position.y - distance, transform.position.z);
    }


    public void Activate()
    {
        transform.position = destination;
    }

    public void Deactivate()
    {
        transform.position = initPosition;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIDownBlack : MonoBehaviour
{
    public static UIDownBlack instance { get; private set; }
    public Image mask;
    Vector3 initPosition;
    public float distance =   174.0f;

    void Awake()
    {
        instance = this;
    }

    private void Start()
    {
        initPosition = this.transform.position;
    }

    public void Activate()
    {
        transform.position = new Vector3(transform.position.x,transform.position.y + distance,transform.position.z);
    }

    public void Deactivate()
    {
        transform.position = initPosition;
    }
}

二者都是为了在分身V被触发时形成屏幕遮照关系,所以可以仅仅通过更改位置的方式控制出现与否。其中distance的初始值174也是一个测出来的经验值。

Attention:实践中发现,在一开始没有确定屏幕的比例情况时,黑幕经常出现distance值不符合情况、黑幕横向长度不足以覆盖整个水平空间等问题。基本上是使用UI改变位置的问题。更好的办法应该是通过alpha值来控制UI出现与否,并且将UI的重心点分配到整个水平空间,这样就可以随着屏幕需求随意更改。

6.场景切换

6.1主菜单MainMenu

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class MainMenu : MonoBehaviour
{
    public void PlayGame()
    {
        SceneManager.LoadScene(1);
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

 在Canvas中MainMenu为UI,其中增加两个Button,其中再增添Text。脚本挂接在MainMenu,botton上挂接Canvas,再选择脚本中想要实现的function。

6.2传送门Door

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Door : MonoBehaviour
{
    public int WantScene = 0;
    private void OnTriggerEnter2D(Collider2D collision)
    {
        SceneManager.LoadScene(WantScene);
    }
}

简单地挂接到一个空物体上,挂接collider作为触发器,并且设置想要去的场景WantScene即可。

7.动画

7.1主角动画PlayerAnimationController

 通过Animation组件创建完动画后,在animator controller环节对动画转化逻辑进行设置。其中包括标准状态、行走、跳跃、冲刺(蹲的部分被砍了)。所有转化都不需要Exit Time,只需要在满足条件的时候播放即可,其中Idle和walk都有loop的特性。

跳跃和冲刺的判断和退出条件很简单,在playercontroller环节进行Jump和Dash两个布尔值的设置。

            //动画相关变量获取
            move = new Vector2(horizontal, 0.0f);
            if (!Mathf.Approximately(move.x,0.0f))
            {
                lookdirection.Set(move.x, 0.0f);
                lookdirection.Normalize();
            }
            animator.SetFloat("MoveX", lookdirection.x);
            animator.SetFloat("Speed", move.magnitude);

由于此处用的是Ruby项目的动画播放更改的,所以保留了对xy两个方向进行记录的可操作部分。行走和标准状态的切换标准也很简单,判断Speed是否大于0.1f即可。至于MoveX用来判断人物此时面对的方向是否为正,从而在四个Blend Tree中进行左右的动作切换。

7.2分身动画SubstitudeController

两个分身的动画逻辑与7.1并无差别,只有素材的更换。除了分身V为了显示与分身E的区别,将color的基础色彩向淡蓝色调节,营造发光效果。

8.声音

8.1SoundManager

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SoundManager : MonoBehaviour
{
    // Start is called before the first frame update
    public static SoundManager instance;//静态实例化
    public AudioSource audioSource;//播放器
    public AudioClip moveAudio, jumpAudio, skillAudio, dashAudio, uiAudio;//不同音效


    private void Awake()
    {
        instance = this;
    }
    public void MoveAudio()
    {
        audioSource.clip = moveAudio;
        audioSource.Play();
    }

    public void JumpAudio()
    {
        audioSource.clip = jumpAudio;
        audioSource.Play();
    }

    public void SkillAudio()
    {
        audioSource.clip = skillAudio;
        audioSource.Play();
    }

    public void DashAudio()
    {
        audioSource.clip = dashAudio;
        audioSource.Play();
    }

    public void UIAudio()
    {
        audioSource.clip = uiAudio;
        audioSource.PlayOneShot(uiAudio);
    }

    void Start()
    {
    
        audioSource = transform.GetComponent<AudioSource>();//完成组件的获取

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.A | KeyCode.D)) //在这里进行一个Input,如果按下键A/D时,播放音效

        {
            audioSource.PlayOneShot(moveAudio);
        }

        if (Input.GetKeyDown(KeyCode.K)) //在这里进行一个Input,如果按下键K时,播放音效

        {
            audioSource.PlayOneShot(jumpAudio);
        }

        if (Input.GetKeyDown(KeyCode.L)) //在这里进行一个Input,如果按下键L时,播放音效

        {
            audioSource.PlayOneShot(dashAudio);
        }

        if (Input.GetKeyDown(KeyCode.J)) //在这里进行一个Input,如果按下键J时,播放音效

        {
            audioSource.PlayOneShot(skillAudio);
        }
    }
}

这部分的代码并非本人编写,但是大体的逻辑就是挂接音频资源。获取输入加以反应,并且在特定时候可以调用单例中的函数,从而播放音频文件。

9.总结

该项目作为一个月的unity学习总结较为完善,包括引擎使用、资源使用、代码优化,甚至是unity团队合作的Unity Plastic SCM使用。但作为一个小的独立游戏项目确实为半成品。主要问题出在沟通上,如果团队不能时刻掌握不同位置的需求,不了解其他人的工作进度,甚至很难在最后的白膜到拼接素材阶段进行工作。这可能有团队不同成员水平层次不齐的问题,但我觉得可以通过一个成熟的项目管理人调节各个部分的工作流程进行优化。一个项目的可变因素可以很多,重要的是找到解决的办法。

如果还有下次参加游戏开发活动,我应该会尝试去作为组长管理整个项目流程,毕竟了解美术资源的使用、白膜构建、脚本编写、关卡设计等,更重要的是看着一个不成熟的项目管理过于折磨了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值