找回密码
 立即注册
查看: 278|回复: 0

制作属于自己的游戏Demo 第一部分。制作自己的角色控制器。

[复制链接]
发表于 2022-6-15 09:41 | 显示全部楼层 |阅读模式
前言:

        我相信在游戏行业的从业人员都会有自己的游戏梦,希望能在自己的职业生涯里制作一款属于自己的游戏,无论大小,是否有趣,希望能与人分享自己的作品,并能得到他人的反馈,当然最好是觉得自己的游戏会很有趣。正好笔者也是一个比较喜欢异想天开的幻想家。所以就趁着这一个月的项目空档期做了一个款还算是比较完整的游戏Demo。满足了一下自己的制作游戏欲望。所以说不要放弃呀同志们,道路是曲折的,未来是光明的。共勉吧!!!!
制作一个游戏Demo需要首先明确的东西。

第一步往往是最难的。因为我们要在一张白纸上进行自己的创作,且必须尽量保证成品的完整和可控性。仅凭一腔热血而没有方法可不行,一定要有步骤有安排的一步步的去完成自己的工作。按部就班可不是啥贬义词,有计划有安排的做事也好做人也罢往往都是事半功倍的。
切忌无限制的去做前期规划和玩法设计。凡事只有去尝试去做才会有变化。过于想像困难或者忽略现实情况都会影响到最后Demo的成品和自己是否能将其完全制作完成。
确定主要的玩法体验相关的制作方向和需求:

1. 确定核心玩法和项目关卡的大小。
范例制作的Demo在初期规划时搭建白模好确定主要的玩法为,在一个小两层的旅馆里发生的找到目标
人物的游戏。可以通过不同的任务线达到最终的目标并找到最后的任务对象。
2. 制作核心的功能和任务链。
首先明确自己的游戏DEMO所需要的主要功能为哪些。范例的Demo所需要的功能主要分为任务激活和
完成,任务的管理系统,NPC和玩家之间的交互,场景道具和任务道具和玩家之间的交互。对话系统。
任务的分支系统以及判断系统等等。
3. 确定主要的任务线的数量。
范例的Demo有一个主要的任务线和2个辅助的任务线支撑,辅助的任务线下又和数个小的支线任务相
关。但最终都会将玩家引导到主要的任务线上去完成最终目标。任务线的梳理一定要清晰,过多的任务
会造成任务的逻辑混乱,极易造成任务的BUG和错误。
确定关卡设计以及场景氛围的美术相关的方向和需求:

一款完整的游戏Demo的美术效果不一定要精美绝伦但一定是要有所取舍。且其中的主要玩法和功能点
必须精炼且一定要完整,给与玩家最完整的游戏体验。BUG肯定也会有。但尽量控制在不会出现在影响
到玩家的核心体验任务上。
1. 确定关卡的分割和NPC的数量以及分布:
游戏中的NPC需要事先确定好分布和数量,普通的NPC和有任务的NPC。每一个NPC的存在意义和其
任务。最好是不要有太多无异议的背景NPC,因为需要单独为其书写文本以及确定和玩家的交互,会造
成不必要的额外工作成本。
2. 确定主要的关卡结构和场景氛围:
初版本来是决定制作一个带室外和室内切换的关卡,但是由于时间的不足因此放弃了室外的制作,因为
主要的故事发生在室内,室外只是一个转场而已。因此可以往后进行延期,场景的氛围设定在夜晚,因
为想营造一个比较昏暗的奇幻中世纪旅店。给与玩家比较好的代入感。
3. 确定角色的风格和性格设定:
初版有过很多的想法和点子,例如加入商人和忍者之类的NPC和玩家交互,甚至加入一些隐藏角色可以
直接帮助玩家达到目标,但后期都一一否定,第一,时间不允许加入太多的角色,因此仅加入了一个愤怒
的武器店老板,玩家的搭档和一个猜谜人的角色。确定好主要角色的数量就可以按部就班的找到类似的
角色资源进行初步的文案书写了。
确定制作Demo所需要的技术和实现方法:

如果说我们把前期工作完成之后就到了最最重头的地方咯。这一块儿可能一篇文章无法完全讲述完成,但
笔者会尽量把自己在制作Demo中所用到的技术和插件的使用方法以及思维逻辑总结提炼出来。供大家进
行讨论。如果有哪些地方讲的不对,希望大佬们多多给以指正。
好的。我们正式开始进行游戏的制作。首先第一步确定自己的Unity版本。



笔者所使用的unity2020.3.9f1的版本并直接下载了官方提供的第三人称控制器工程


利用官方提供的范例工程我们可以很方便的拥有了一个预设好的范例场景。在这个场景里玩家可以控制这个塑料小人跳跃和移动。这样第一步就搞定了玩家的控制系统。但是这也给我们后期的工作埋下了一个比较大的隐患。毕竟这世界上免费的东西才是最贵的。
最后自己挖的坑还是得自己填哟。
Unity的新版输入系统:

可以在PackageManager里面找到这个官方提供的插件。


Unity提供的新版输入系统可以很方便的进行键位的绑定。并且还会可以自己定义ActionMaps进行区分。方便进行复杂的键位管理。而且每一个键位还可以进行进一步的根据按下。持续时间和弹起。来分别进行事件绑定。相当的好用。(不过依旧需要一定的代码编译能力,接下来就开始手动制作一个我们自己的角色控制器。)





首先需要新建一个专用的输入控制文件。并在玩家的gameobject身上挂载相应的控制组件。



新建的输入控制是空的。因此需要我们自己去添加键位绑定


先制作最简单的移动功能。设定好ActionMaps和Actions键位绑定就可以开始进行最基础的代码编译工作了。
先自定义一个自己的控制器脚本。运用新版的输入系统需要先加入using UnityEngine.InputSystem
官方有提供输入的模板脚本(还是挺方便的。不愧是官方的程序员写的范例脚本):
安装Cinemachine插件:




官方提供的摄像机控件。可以很方便的帮助我们调整摄像机的位置。



主摄像机需要挂上CinemachineBrain帮助Cinemachine系统来进行识别



新建一个GameObject并挂载插件来进行详细的追踪摄像机设置。

制作一个我们自己的角色控制器。

//using System.Collections;
//using System.Collections.Generic;
using TestInputAssets;
using UnityEngine;
using UnityEngine.InputSystem;

public class TestUseContraller : MonoBehaviour
{
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
    private PlayerInput playerInput;
#endif
    private TestAssetInputSystem input;

    private CharacterController controller;
    private Vector3 playerVelocity;
    private bool groundedPlayer;
    [SerializeField]
    private float playerSpeed = 2.0f;
    //[SerializeField]
    //private float jumpHeight = 1.0f;
    //[SerializeField]
    //private float gravityValue = -9.81f;


    // cinemachine
    private float _cinemachineTargetYaw;
    private float _cinemachineTargetPitch;

            
    private float _targetRotation = 0.0f;
    private float _rotationVelocity;
    private float _verticalVelocity;
    private float _speed;
    private float _terminalVelocity = 53.0f;

    private GameObject _mainCamera;//设置一下我们需要进行抓取的主要摄像机

    private const float threshold = 0.01f;


    // timeout deltatime
    private float _jumpTimeoutDelta;
    private float _fallTimeoutDelta;

    [Tooltip("Acceleration and deceleration")]
    public float SpeedChangeRate = 10.0f;

    [Tooltip("How fast the character turns to face movement direction")]
    [Range(0.0f, 0.3f)]
    public float RotationSmoothTime = 0.12f;

    [Header("Cinemachine")]
    [Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
    public GameObject CinemachineCameraTarget;

    [Tooltip("For locking the camera position on all axis")]
    public bool LockCameraPosition = false;

    [Tooltip("How far in degrees can you move the camera up")]
    public float TopClamp = 70.0f;

    [Tooltip("How far in degrees can you move the camera down")]
    public float BottomClamp = -30.0f;

    [Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]
    public float CameraAngleOverride = 0.0f;
    public float CameraMoveSpeed = 15.0f;


    [Space(10)]
    [Tooltip("The height the player can jump")]
    public float JumpHeight = 1.2f;

    [Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
    public float Gravity = -15.0f;

    [Space(10)]
    [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
    public float JumpTimeout = 0.50f;

    [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
    public float FallTimeout = 0.15f;


    [Header("Player Grounded")]
    [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
    public bool Grounded = true;

    [Tooltip("Useful for rough ground")]
    public float GroundedOffset = -0.14f;

    [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
    public float GroundedRadius = 0.28f;

    [Tooltip("What layers the character uses as ground")]
    public LayerMask GroundLayers;

    private void Awake()
    {
        // get a reference to our main camera
        if (_mainCamera == null)
        {
            _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
        }
    }


    private void LateUpdate()//写入稍后执行的一些动作
    {
        CameraRotation();
        GroundedCheck();
    }

    private void GroundedCheck()//检查角色的地面
    {
        // set sphere position, with offset
        Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
            transform.position.z);
        Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
            QueryTriggerInteraction.Ignore);

        //// update animator if using character
        //if (_hasAnimator)
        //{
        //    _animator.SetBool(_animIDGrounded, Grounded);
        //}
    }

    private void Start()
    {
        input = GetComponent<TestAssetInputSystem>();
        controller = GetComponent<CharacterController>();

         #if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
             playerInput = GetComponent<PlayerInput>();
#else
                  Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
#endif


        // reset our timeouts on start
        _jumpTimeoutDelta = JumpTimeout;
        _fallTimeoutDelta = FallTimeout;

    }

    private bool IsCurrentDeviceMouse
    {
        get
        {
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
            return playerInput.currentControlScheme == "KeyboardMouse";
#else
                                return false;
#endif
        }
    }

    void Update()
    {
        groundedPlayer = controller.isGrounded;
        if (groundedPlayer && playerVelocity.y < 0)
        {
            playerVelocity.y = 0f;
        }

        // set target speed based on move speed, sprint speed and if sprint is pressed
        float targetSpeed = playerSpeed;


        // a reference to the players current horizontal velocity
        float currentHorizontalSpeed = new Vector3(controller.velocity.x, 0.0f, controller.velocity.z).magnitude;
        float speedOffset = 0.1f;

        // a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon

        // note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
        // if there is no input, set the target speed to 0
        if (input.move == Vector2.zero) targetSpeed = 0.0f;
        // accelerate or decelerate to target speed
        if (currentHorizontalSpeed < targetSpeed - speedOffset ||
            currentHorizontalSpeed > targetSpeed + speedOffset)
        {
            // creates curved result rather than a linear one giving a more organic speed change
            // note T in Lerp is clamped, so we don't need to clamp our speed
            _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed,
                Time.deltaTime * SpeedChangeRate);

            // round speed to 3 decimal places
            _speed = Mathf.Round(_speed * 1000f) / 1000f;
        }
        else
        {
            _speed = targetSpeed;
        }

        Vector3 inputDirection = new Vector3(input.move.x, 0.0f, input.move.y).normalized;

        // note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
        // if there is a move input rotate player when the player is moving
        if (input.move != Vector2.zero)
        {
            _targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
                              _mainCamera.transform.eulerAngles.y;
            float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
                RotationSmoothTime);

            // rotate to face input direction relative to camera position
            transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
        }


        Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;

        // move the player
        controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
                         new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);


        //if (move != Vector3.zero)
        //{
        //    gameObject.transform.forward = move;
        //}

        // Changes the height position of the player..

        controller.Move(playerVelocity * Time.deltaTime);
    }
    private void CameraRotation()
    {
        // if there is an input and camera position is not fixed
        if (input.look.sqrMagnitude >= threshold && !LockCameraPosition)
        {
            //Don't multiply mouse input by Time.deltaTime;
            float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;

            _cinemachineTargetYaw += input.look.x * deltaTimeMultiplier * CameraMoveSpeed;
            _cinemachineTargetPitch += input.look.y * deltaTimeMultiplier * CameraMoveSpeed;
        }

        // clamp our rotations so our values are limited 360 degrees
        _cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
        _cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

        // Cinemachine will follow this target
        CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
            _cinemachineTargetYaw, 0.0f);

    }

    private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
    {
        if (lfAngle < -360f) lfAngle += 360f;
        if (lfAngle > 360f) lfAngle -= 360f;
        return Mathf.Clamp(lfAngle, lfMin, lfMax);
    }

   
}
新定义一份传输InputSystem的数据和监控数据传输与否而用的脚本。
using UnityEngine;
#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED
using UnityEngine.InputSystem;
#endif

namespace TestInputAssets
{
        public class TestAssetInputSystem : MonoBehaviour
        {
                [Header("Character Input Values")]
                public Vector2 move;
                public Vector2 look;
                //public bool sprint;

                //[Header("Movement Settings")]
                //public bool analogMovement;

                [Header("Mouse Cursor Settings")]
                public bool cursorLocked = true;
                public bool cursorInputForLook = true;

#if ENABLE_INPUT_SYSTEM && STARTER_ASSETS_PACKAGES_CHECKED//新增获取输入模块的数据
                public void OnMove(InputValue value)
                {
                        MoveInput(value.Get<Vector2>());
                }

                public void OnLook(InputValue value)
                {
                        if (cursorInputForLook)
                        {
                                LookInput(value.Get<Vector2>());
                        }
                }


                //public void OnSprint(InputValue value)
                //{
                //        SprintInput(value.isPressed);
                //}
#endif


                public void MoveInput(Vector2 newMoveDirection)
                {
                        move = newMoveDirection;
                }

                public void LookInput(Vector2 newLookDirection)
                {
                        look = newLookDirection;
                }


                //public void SprintInput(bool newSprintState)
                //{
                //        sprint = newSprintState;
                //}

                private void OnApplicationFocus(bool hasFocus)
                {
                        SetCursorState(cursorLocked);
                }

                private void SetCursorState(bool newState)
                {
                        Cursor.lockState = newState ? CursorLockMode.Locked : CursorLockMode.None;
                }
        }

}
设置输入用的屏幕手柄控件。



利用On-Screen Strick来绑定UI键位

OK这样一个最基础的移动和可转动视角的基础控制器已经制作完成。代码会比较繁琐和不规范。后期在慢慢修正。初期的控制器最好不要写的太复杂。功能可以在后期慢慢加入。
总的来说。制作一个自己的游戏DEMO的第一步已经完成。之后再加入和场景物件的交互功能。和NPC的交互功能以及一些任务系统之类的玩家体验相关的部分一步步的完善我们的作品。切忌不要心急。按部就班的进行,因为很多时间我们前期制作的一些功能和工具很可能到了后期想要加入新的想法和点子后反而成了鸡肋或者无效的工作。因此各个部分的功能没必要完全写到完美,而是维持一个有用且可以解决问题的状态即可,代码的编译最好还是有空就好好整理一下,不然到了后期可能咱们自己都会有些看不懂。
范例工程连接:

下一篇文章会开始介绍一下如何进行场景制作上和美术在工程设置上需要注意的一些点。
范例工程仅供学习使用,切勿直接用于正式项目。
MoveTest.unitypackage
13.6K
· 百度网盘

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-3 14:47 , Processed in 0.249775 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表