《Unity In Action》读书笔记

Unity最大的优点:可视化的工作流和跨平台的支持。Unity基于component的设计,使得一个component能被重复的使用。

Unity的缺点有:查找功能不够强大,有时候在项目中查找脚本比较麻烦;不支持链接到第三方库,要使用时必须手动拷贝到工程中来;prefab是Unity独有的重要功能,但是编辑prefab又不太方便。这些都希望在以后的版本中得到改进。

Unity开发的产品横跨三大平台(主机、PC、手机),比较著名的有:炉石传说、王者荣耀、神庙逃亡、捣蛋猪(愤怒的小鸟)、高尔夫俱乐部等。

Unity推荐的官方开发语言有C#和JavaScript,这里的JavaScript与标准版有所不同。

所有的Unity脚本都要继承MonoBehaviour,它提供了Start()和Update()方法以供重载。前者是在脚本关联的对象被激活时(也就是对象所在层级被加载时)调用。

构建一个FPS游戏场景需要:创建场景和人物,放置光源,将camera与人物绑定,处理移动脚本。

以大拇指为x轴,食指y轴,中指z轴,可将三维坐标系分为:左手系和右手系。Unity使用的是左手系,OpenGL用的是右手系。左手系和右手系区别在于:左手系的z轴由近及远,右手系的z轴由远及近。

Unity的对象层级结构是树状的,对象与对象之间呈父子关系。当没有合适的根对象时我们可以创建空的根对象。这里指的对象都是GameObject,它实际上是一系列component的容器,装载的component实际上决定了GameObject的作用。

Unity支持三种类型的光源:点光源(point)、聚光灯(spot)、平行光(directional)。分别类似于灯泡、手电筒、阳光。point和spot可以指定光照范围,它们比directional带来更多的内存消耗。离point越近照得越亮。

创建的Cube对象会自带如下基础component:Transform,Mesh Filter(几何形状),Box Collider(处理碰撞),Mesh Renderer(负责网格的渲染)。

Capsule默认自带capsule collider,这里移除并替换成character controller,为了更好地模拟人的动作。

CharacterController是Unity官方提供的模拟人动作的组件,特点是不受力的控制,只受碰撞影响。

将camera绑定在角色身上,设置位置为(0, 0.5, 0),让其跟着角色移动。

Rotate()方法默认使用本地坐标系,若需使用全局坐标系需额外注明:Rotate(0, speed, 0, Space.World)

下面是视角随鼠标旋转的代码:

// x轴上的变化对应的是y轴上的旋转
float rotationY = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * sensitivityHor;
_rotationX -= Input.GetAxis("Mouse Y") * sensitivityVert;
// 对于x轴上的旋转,应控制范围
_rotationX = Mathf.Clamp(_rotationX, minimumVert, maximumVert);
// 将新的向量赋值给欧拉角
transform.localEulerAngles = new Vector3(_rotationX, rotationY, 0);

localRotation使用的是四元数,localEulerAngles使用的是欧拉角,两者可以互相转换。四元数的优点是:避免万向节锁,提供平滑插值,表达效率高。

Rigidbody中的freezeRotation属性可以禁用物理模拟引起的转动。

下面是位置随键盘输入移动的代码:

void Start() {
    // 要做碰撞必须使用CharacterController
    _charController = GetComponent<CharacterController>();
}
void Update() {
    float deltaX = Input.GetAxis("Horizontal") * speed;
    float deltaZ = Input.GetAxis("Vertical") * speed;
    Vector3 movement = new Vector3(deltaX, 0, deltaZ);
    // 斜线运动时不能超过最大速度
    movement = Vector3.ClampMagnitude(movement, speed);
    // 避免飞起来
    movement.y = gravity;
    // 考虑避免不同帧数的影响
    movement *= Time.deltaTime;
    // 从本地坐标系转成全局坐标系(若是用Translate()方法则是用本地坐标系,但是没有碰撞)
    movement = transform.TransformDirection(movement);
    _charController.Move(movement);
}

可以通过RequireComponent和AddComponentMenu声明来标注component依赖关系,使得component之间能够相互调用。

子弹的轨迹是由2D坐标屏幕的camera位置射向3D坐标世界的某个点(沿视椎体方向)。

Unity中的协程不是异步的,yield之后退出,下一帧再接着执行。执行时机是在update()和lateUpdate()之间。适合用于耗时操作:如等待几秒后销毁,文件加载好后处理。

发射子弹并击中目标的代码:

    void Start() {
        _camera = GetComponent<Camera>();
        // 鼠标锁定且隐藏,按ESC解除锁定
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;
    }
    // 在每一帧完成3D场景渲染后调用
    void OnGUI() {
        int size = 12;
        float posX = _camera.pixelWidth/2 - size/4;
        float posY = _camera.pixelHeight/2 - size/2;
        // 这里用基础GUI构造了一个TextLabel
        GUI.Label(new Rect(posX, posY, size, size), "*");
    }

    void Update() {
        if (Input.GetMouseButtonDown(0)) {
            // 从camera中心发出
            Vector3 point = new Vector3(_camera.pixelWidth/2, _camera.pixelHeight/2, 0);
            // 这个方法垂直屏幕发出射线
            Ray ray = _camera.ScreenPointToRay(point);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit)) {
                // 击中的对象
                GameObject hitObject = hit.transform.gameObject;
                ReactiveTarget target = hitObject.GetComponent<ReactiveTarget>();
                // 若找到相应脚本,则认为是敌人,否则是普通物体
                if (target != null) {
                    target.ReactToHit();
                } else {
                    StartCoroutine(SphereIndicator(hit.point));
                }
            }
        }
    }

    // 调用协程方法
    private IEnumerator SphereIndicator(Vector3 pos) {
        GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        sphere.transform.position = pos;
        // 暂停1秒再销毁(直接return, Unity会延迟1秒再执行下面的Destroy)
        yield return new WaitForSeconds(1);

        Destroy(sphere);
    }

tweens可以让敌人被击中后倾斜的动作更加顺滑。

敌人AI代码:

    void Start() {
        _alive = true;
    }

    void Update() {
        if (_alive) {
            // 1. 向前移动(rotate()和translate()都默认使用本地坐标系)
            transform.Translate(0, 0, speed * Time.deltaTime);

            Ray ray = new Ray(transform.position, transform.forward);
            RaycastHit hit;
            if (Physics.SphereCast(ray, 0.75f, out hit)) {
                GameObject hitObject = hit.transform.gameObject;
                // 2. 若发射线击中物体了
                if (hitObject.GetComponent<PlayerCharacter>()) {
                    // 若没有火球则产生火球
                    if (_fireball == null) {
                        _fireball = Instantiate(fireballPrefab) as GameObject;
                        _fireball.transform.position = transform.TransformPoint(Vector3.forward * 1.5f);
                        _fireball.transform.rotation = transform.rotation;
                    }
                }
                // 3. 若在一定范围内
                else if (hit.distance < obstacleRange) {
                    // 4. 则旋转随机角度
                    float angle = Random.Range(-110, 110);
                    transform.Rotate(0, angle, 0);
                }
            }
        }
    }

prefab是可被不同场景重复使用的GameObject,它可以看做是一种asset,在场景中需要使用时动态加载,这时产生的一个copy叫做instance。关键词:动态加载,重复使用。

敌人死亡后重生的做法是:创建一个空GameObject,下面挂载一个脚本SceneController,每帧判断下当前场景中是否有enemy prefab的instance,没有则创建。

火球运动的代码:

    void Update() {
        transform.Translate(0, 0, speed * Time.deltaTime);
    }

    // 碰撞时触发(需要设置rigidBody)
    void OnTriggerEnter(Collider other) {
        PlayerCharacter player = other.GetComponent<PlayerCharacter>();
        if (player != null) {
            // 执行扣血代码
            player.Hurt(damage);
        }
        Destroy(this.gameObject);
    }

敌人和玩家子弹的处理不一样,一个是用prefab,可以看到移动过程;另一个是raycast,瞬间击中。

art asset的类型分类:material, texture, model, animation, particle system。

Unity支持的2D图形文件类型:PNG,JPG,GIF,BMP,TGA,TIFF,PICT,PSD。其中PNG是无损压缩,JPG和GIF是有损压缩。Unity中推荐使用的格式是:PNG,TGA和PSD。

将texture应用到model的方法:可以直接拖拽到model上,这时material会自动生成;也可以先创建material,再将texture和material关联。

Unity支持的3D模型格式有:FBX,Collada,OBJ,3DS,DXF,Maya,3ds Max,Blender。推荐的是FBX和Collada,它们同时支持mesh和animation。

粒子系统用于表现大量物体的运动(如火焰、烟雾、水)。

sprite是直接在屏幕上显示的2D图片,而非贴在3D模型表面(texture)。拖入2D场景时自动设为sprite类型。

SendMessage()可以向指定对象通信,效率低于直接调用指定对象的方法,但是无需知道它的类型信息。

Application.LoadLevel()可以用来重新加载场景。

展示2D图形需要用orthographic(正交)摄像机。

对于pixel-perfect的图形,摄像机的大小应该是屏幕高度一半。

sprite若要支持点击,则需要在其上加2D collider。

UI text有多种创建方法,其中一种是通过创建3D text对象。

翻牌游戏相关代码片段:

初始化所有牌的代码:

        // place cards in a grid
        for (int i = 0; i < gridCols; i++) {
            for (int j = 0; j < gridRows; j++) {
                MemoryCard card;

                // use the original for the first grid space
                if (i == 0 && j == 0) {
                    card = originalCard;
                } else {
                    // Instantiate方法可以clone自定义类型的对象
                    card = Instantiate(originalCard) as MemoryCard;
                }

                // next card in the list for each grid space
                int index = j * gridCols + i;
                int id = numbers[index];
                card.SetCard(id, images[id]);

                float posX = (offsetX * i) + startPos.x;
                float posY = -(offsetY * j) + startPos.y;
                card.transform.position = new Vector3(posX, posY, startPos.z);
            }
        }

检查是否匹配的代码:

    private IEnumerator CheckMatch() {

        // increment score if the cards match
        if (_firstRevealed.id == _secondRevealed.id) {
            _score++;
            scoreLabel.text = "Score: " + _score;
        }

        // otherwise turn them back over after .5s pause
        else {
            yield return new WaitForSeconds(.5f);

            _firstRevealed.Unreveal();
            _secondRevealed.Unreveal();
        }

        _firstRevealed = null;
        _secondRevealed = null;
    }

GUI系统分为immediate mode和retained mode两种:前者每帧都会重绘(在OnGUI()方法中),使用简单但功能单一;后者是更新的方法,需要使用Canvas在场景中编辑,可支持更复杂的功能。

Canvas是Unity中用来绘制UI的特殊object。创建它时会自动加入EventSystem component。

UI的anchor可设置相对屏幕角的位置,这样屏幕缩放后相对位置也不会变。

sliced image:九宫格切片,切成九块,中间缩放,其余不变。这里的弹出窗口就是sliced image。

PlayerPrefs可以用来持久化存储应用的数据。

使用event来处理UI和场景之间的交互,好处是降低耦合性。如何使用event:1. 定义event;2.A模块广播event;3.B模块监听event并做处理。

阴影分为实时阴影和光照贴图。前者效果好,但消耗计算量大;后者是由烘焙出来的纹理贴图而成。一般来说,静态物体适合用光照贴图,动态物体(如人物)适合用实时阴影。

LateUpdate()调用时机是在所有对象的Update()调用完成之后。

在悬崖和边缘处用射线来处理地面探测,真实性更好。

animation由animation clip作为基本元素组成,animation controller是状态机,定义了各状态之间的转换条件。

TPS中camera绑定的代码:

    // Use this for initialization
    void Start() {
        _rotY = transform.eulerAngles.y;
        _offset = target.position - transform.position;
    }

    // Update is called once per frame
    // 用LateUpdate是因为要跟着人来走
    void LateUpdate() {
        // 无论是转视角还是水平移动,都需要绕着player旋转camera
        float horInput = Input.GetAxis("Horizontal");
        if (horInput != 0) {
            _rotY += horInput * rotSpeed;
        } else {
            _rotY += Input.GetAxis("Mouse X") * rotSpeed * 3;
        }

        Quaternion rotation = Quaternion.Euler(0, _rotY, 0);
        // 根据player的位置和旋转角度,来调整camera位置,并保持相对位置
        transform.position = target.position - (rotation * _offset);
        // camera转向player
        transform.LookAt(target);
    }

TPS中player绑定的代码:

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

        // start with zero and add movement components progressively
        Vector3 movement = Vector3.zero;

        // x z movement transformed relative to target
        float horInput = Input.GetAxis("Horizontal");
        float vertInput = Input.GetAxis("Vertical");
        if (horInput != 0 || vertInput != 0) {
            movement.x = horInput * moveSpeed;
            movement.z = vertInput * moveSpeed;
            movement = Vector3.ClampMagnitude(movement, moveSpeed);

            Quaternion tmp = target.rotation;
            target.eulerAngles = new Vector3(0, target.eulerAngles.y, 0);
            // 将基于camera的移动转成global的移动
            movement = target.TransformDirection(movement);
            target.rotation = tmp;

            // face movement direction
            //transform.rotation = Quaternion.LookRotation(movement);
            // 往移动方向转向
            Quaternion direction = Quaternion.LookRotation(movement);
            transform.rotation = Quaternion.Lerp(transform.rotation,
                                                 direction, rotSpeed * Time.deltaTime);
        }
        // 设置speed属性,触发进入动画的walk状态
        _animator.SetFloat("Speed", movement.sqrMagnitude);

        // raycast down to address steep slopes and dropoff edge
        bool hitGround = false;
        RaycastHit hit;
        // 利用射线更精确地做地面接触检测
        if (_vertSpeed < 0 && Physics.Raycast(transform.position, Vector3.down, out hit)) {
            float check = (_charController.height + _charController.radius) / 1.9f;
            hitGround = hit.distance <= check;  // to be sure check slightly beyond bottom of capsule
        }

        // y movement: possibly jump impulse up, always accel down
        // could _charController.isGrounded instead, but then cannot workaround dropoff edge
        // 若射线检测在地面
        if (hitGround) {
            if (Input.GetButtonDown("Jump")) {
                _vertSpeed = jumpSpeed;
            } else {
                _vertSpeed = minFall;
                // 落到地面后停止jump动作
                _animator.SetBool("Jumping", false);
            }
        } else {
            _vertSpeed += gravity * 5 * Time.deltaTime;
            if (_vertSpeed < terminalVelocity) {
                _vertSpeed = terminalVelocity;
            }
            // 实际没有碰撞
            if (_contact != null ) {    // not right at level start
                _animator.SetBool("Jumping", true);
            }

            // workaround for standing on dropoff edge
            // 实际在地面上(悬崖边)
            if (_charController.isGrounded) {
                if (Vector3.Dot(movement, _contact.normal) < 0) {
                    movement = _contact.normal * moveSpeed;
                } else {
                    movement += _contact.normal * moveSpeed;
                }
            }
        }
        movement.y = _vertSpeed;

        movement *= Time.deltaTime;
        _charController.Move(movement);
    }

    // store collision to use in Update
    void OnControllerColliderHit(ControllerColliderHit hit) {
        _contact = hit;
    }

trigger可以定义触发操作,它不是真实的物体,可以被穿透。

Awake()比Start()更早执行,它可以做一些初始化操作。

Resources.Load()可以从Resources目录加载一些assets。

人和场景中物体交互的一些代码:

Player上面绑定的代码:

    void Update() {
        // 若按下command键
        if (Input.GetButtonDown("Fire3")) {
            Collider[] hitColliders = Physics.OverlapSphere(transform.position, radius);
            foreach (Collider hitCollider in hitColliders) {
                Vector3 direction = hitCollider.transform.position - transform.position;
                if (Vector3.Dot(transform.forward, direction) > .5f) {
                    // 则向周围一定半径的物体发送Operate消息
                    hitCollider.SendMessage("Operate", SendMessageOptions.DontRequireReceiver);
                }
            }
        }
    }

门的trigger上绑定的代码:

    void OnTriggerEnter(Collider other) {
        if (requireKey && Managers.Inventory.equippedItem != "key") {
            return;
        }

        foreach (GameObject target in targets) {
            // 碰撞后,对门发送Activate消息
            target.SendMessage("Activate");
        }
    }

    void OnTriggerExit(Collider other) {
        foreach (GameObject target in targets) {
            target.SendMessage("Deactivate");
        }
    }

门上绑定的代码:

    // 无论是通过按键,还是碰撞,都可以实现门的开闭
    public void Operate() {
        if (_open) {
            Vector3 pos = transform.position - dPos;
            transform.position = pos;
        } else {
            Vector3 pos = transform.position + dPos;
            transform.position = pos;
        }
        _open = !_open;
    }

    public void Activate() {
        if (!_open) {
            Vector3 pos = transform.position + dPos;
            transform.position = pos;
            _open = true;
        }
    }
    public void Deactivate() {
        if (_open) {
            Vector3 pos = transform.position - dPos;
            transform.position = pos;
            _open = false;
        }
    }

场景中散落的道具上绑定的代码:

    void OnTriggerEnter(Collider other) {
        // 碰撞之后ui上增加,并将自己销毁
        Managers.Inventory.AddItem(itemName);
        Destroy(this.gameObject);
    }

UI相关的代码:

    // 实时绘制UI
    void OnGUI() {
        int posX = 10;
        int posY = 10;
        int width = 100;
        int height = 30;
        int buffer = 10;

        List<string> itemList = Managers.Inventory.GetItemList();
        if (itemList.Count == 0) {
            GUI.Box(new Rect(posX, posY, width, height), "No Items");
        }
        foreach (string item in itemList) {
            int count = Managers.Inventory.GetItemCount(item);
            Texture2D image = Resources.Load<Texture2D>("Icons/"+item);
            GUI.Box(new Rect(posX, posY, width, height), new GUIContent("(" + count + ")", image));
            posX += width+buffer;
        }

        string equipped = Managers.Inventory.equippedItem;
        if (equipped != null) {
            posX = Screen.width - (width+buffer);
            Texture2D image = Resources.Load("Icons/"+equipped) as Texture2D;
            GUI.Box(new Rect(posX, posY, width, height), new GUIContent("Equipped", image));
        }

        posX = 10;
        posY += height+buffer;

        foreach (string item in itemList) {
            // 创建button同时判断button是否被按下
            if (GUI.Button(new Rect(posX, posY, width, height), "Equip "+item)) {
                Managers.Inventory.EquipItem(item);
            }

            if (item == "health") {
                if (GUI.Button(new Rect(posX, posY + height+buffer, width, height), "Use Health")) {
                    Managers.Inventory.ConsumeItem("health");
                    Managers.Player.ChangeHealth(25);
                }
            }

            posX += width+buffer;
        }
    }

Trigger和Collider的区别:Collider是物理碰撞,isTrigger是Collider上的一个属性,勾选后只检测碰撞,但没有物理效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值