《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上的一个属性,勾选后只检测碰撞,但没有物理效果。

Unity in Action, Second Edition is a book about programming games in Unity. Think of it as an intro to Unity for experienced programmers. The goal of this book is straightfor- ward: to take people who have some programming experience but no experience with Unity and teach them how to develop a game using Unity. The best way of teaching development is through example projects, with students learning by doing, and that’s the approach this book takes. I’ll present topics as steps toward building sample games, and you’ll be encouraged to build these games in Unity while exploring the book. We’ll go through a selection of different projects every few chapters, rather than one monolithic project developed over the entire book. (Some- times other books take the “one monolithic project” approach, but that can make it hard to jump into the middle if the early chapters aren’t relevant to you.) This book will have more rigorous programming content than most Unity books (especially beginners’ books). Unity is often portrayed as a list of features with no pro- gramming required, which is a misleading view that won’t teach people what they need to know in order to produce commercial titles. If you don’t already know how to pro- gram a computer, I suggest going to a resource like Codecademy first (the computer programming lessons at Khan Academy work well, too) and then come back to this book after learning how to program. Don’t worry about the exact programming language; C# is used throughout this book, but skills from other languages will transfer quite well. Although the first part of the book will take its time introducing new concepts and will carefully and deliberately
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值