East's Blog

C# 初级编程-Unity

变量和函数(Variables and Functions)

int myInt = 5;

void Start() {
    myInt = MultiplyByTwo(myInt);
    Debug.Log(myInt)
}

int MultiplyByTwo(int number) {
    int result;
    result = number * 2;
    return result;
}

IF 语句(If Statements)

float coffeeTemperature = 85.0f;
float hotLimitTemperature = 70.0f;
float coldLimitTemperature = 40.0f;

void Update()
{
    if(Input.GetKeyDown(KeyCode.Space))
        TemperatureTest();
    
    coffeeTemperature -= Time.deltaTime * 5f;
}

void TemperatureTest()
{
    if(coffeeTemperature > hotLimitTemperature)
    {
        print("Coffee is too hot.")
    }
    else if (coffeeTemperature < coldLimitTemperature)
    {
        print("Coffee is too cold.")
    }
    else
    {
        print("Coffee is just right.")
    }
}

循环(Loops)

循环是编程中重复操作的方式。

3中不同的循环类型:ForLoop, WhileLoop 和 DoWhileLoop。

WhileLoop

在满足条件时执行操作

int cupsInTheSink = 4;

void Start()
{
    while (cupsInTheSink > 0)
    {
        Debug.Log("I've washed a cup!");
        cupsInTheSink--;
    }
}

DoWhileLoop

DoWhileLoop 的功能与 WhileLoop 几乎一样,只有一个明显区别,WhileLoop 在循环主体前检验条件,但 DoWhileLoop 在循环主体结束时检验条件。

这个区别意味着 DoWhileLoop 主体至少会运行一次。

void Start()
{
    bool shouldContinue = false;

    do
    {
        print("Hello DoWhileLoop")
    }
    while (shouldContinue == true);
}

ForLoop

ForLoop 或许是最常见最灵活的循环,ForLoop 利用可控数量的迭代创建循环。就功能而言,它会先检查循环中的条件,

int numEnemies = 3;

void Start()
{
    for (int i = 0; i < numEnemies; i++)
    {
        Debug.Log("Creating enemy number: " + i);
    }
}

作用域和访问修饰符(Scope and Access Modifiers)

变量作用域:代码中可以使用这个变量的区域

代码块通常用于定义变量作用域,用花括号表示。

public class ScopeAndAccessModifiers : MonoBehaviour
{
    public int alpha;

    private int beta = 0;
    private int gamma = 5;

    void Example(int pens, int crayons)
    {
        int answer;
        answer = pens * crayons * alpha;
        Debug.Log(answer);
    }

    void Update()
    {
        Debug.Log("Alpha is set to: " + alpha);
    }
}

例如,这个类里的所有内容,都可以称为该类的局部代码。

公开和私有访问修饰符

类中定义的变量不同于函数内声明的变量,类拥有访问修饰符。访问修饰符是在声明变量时放在数据类型前的关键字,其用途是定义能否看到变量或函数。

将变量设为 public 意味着可以从类外部访问这个变量。也意味着可以通过其它脚本访问它们,

私有变量只能在类内编辑,在 C# 中未指定访问修饰符的任意变量,默认使用 private 访问修饰符。

Awake 和 Start

public class AwakeAndStart : MonoBehaviour
{
    void Awake()
        // References between scripts, initialization
    {
        Debug.Log("Awake called.");
    }

    void Start()
        // Once script component is enabled
    {
        Debug.Log("Start called.");
    }
}

AwakeStart 是在加载脚本时自动调用的两个函数。

首先调用 Awake 即使还未启用脚本组件,它非常适用于在脚本与初始化之间设置任何引用。

StartAwake 之后调用,而且是直接在首次更新之前调用,前提是已经启用了脚本组件。在启用了脚本组件的情况下,可以用 Start 启动任何所需操作。这样就可以将初始化代码的任何部分延迟到真正需要的时候再运行。

Start 和 Await 在一个对象绑定脚本的生命周期内只能调用一次。

Update 和 FixedUpdate

private float fixedUpdateTimer;
private float UpdateTimer;

void FixedUpdate()
{
    Debug.Log("FixedUpdate time :" + Time.deltaTime);
}

void Update()
{
    Debug.Log("Update time :" + Time.deltaTime);
}

Update 是 Unity 中最常用的函数之一。在每个使用它的脚本中,每帧调用一次。基本上,只要是需要变化或调整都需要使用 Update 来实现。非物理对象的移动,简单的计时器,输入检测等等,一般都在 Update 中完成。

注意,Update 并不是按固定的时间调用的,如果某一帧比下一帧处理时间长,那么 Update 调用的时间间隔就会不同。

FixedUpdate 函数与 Update 类似,但有几点明显不同。

FixedUpdate 按固定时间调用。调用 FixedUpdate 之后,会立即进行任何必要的物理计算,因此,任何影响刚体(即物理对象)的动作都应使用 FixedUpdate 执行。

在 FixedUpdate 循环中编写物理脚本时,最好使用力来定义移动。

Ctrl + Shift + M 启动向导,勾选要添加的方法名。

矢量数学

启用和禁用组件

启用和禁用 Unity 中的组件,只需要使用 enabled 标记。

public class EnableComponents : MonoBehaviour {
    private Light myLight;

    void Start()
    {
        myLight = GetComponent<Light>()
    }

    void Update()
    {
        if(Input.GetKeyUp(KeyCode.Space))
        {
            myLight.enabled = !myLight.enabled;
        }
    }
}

激活游戏对象

要通过脚本激活或停用对象,可以使用 SetActive 函数。

void Start()
{
    gameObject.SetActive(false);
}

要确认某个对象在场景或在层次结构中是否为活跃状态,可以使用 Active Self 和 Active in Hierarchy 状态查询。

public class CheckState : MonoBehaviour
{
    public GemeObject myObject;

    void Start()
    {
        Debug.Log("Active Self: " + myObject.activeSelf);
        Debug.Log("Active in Hierarchy" + myObject.activeInHierarchy);
    }
}

在层次结构中,如果停用父对象,子对象被激活,但所处的层次结构未被激活。

平移和旋转(Translate and Rotate)

平移和旋转是两个常用函数,用来更改游戏对象的位置和旋转。

void Update()
{
    transform.Translate(new Vector3(0, 0, 1));
}

通常在使用平移操作时,会乘以 Time.deltaTime,这意味着它会按每秒多少米的速度移动,而不是每帧多少米。

public float moveSpeed = 10f;

void Update()
{
    transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
}

按下某个键时发生运动

public float moveSpeed = 10f;

void Update()
{
    if(Input.GetKey(KeyCode.UpArrow))
        transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    
    if(Input.GetKey(KeyCode.DownArrow))
        transform.Translate(-Vector3.forward * moveSpeed * Time.deltaTime);
}

transform.Rotate

public float turnSpeed = 50f;

void Update()
{
    if(Input.GetKey(KeyCode.LeftArrow))
        transform.Rotate(Vector3.up, -turnSpeed * Time.deltaTime);
    
    if(Input.GetKey(KeyCode.RightArrow))
        transform.Rotate(Vector3.up, turnSpeed * Time.deltaTime);
}

应当注意,这些函数作用于局部轴而非世界轴。所以使用 Vector3.Forward 或 Vector3.Up 时,相对的是脚本所应用到的游戏对象的轴。

如果想用碰撞体移动某个对象,也就是将会产生物理作用的物体,则不应该使用 Translate 和 Rotate 函数,而应该考虑使用 Physics 函数。

LookAt

LookAt 用于让游戏对象的正向指向世界中的另一个 transform.

让镜头对准正在掉落的对象

public Transform target;

void Update()
{
    transform.LookAt(target)
}

Destory

Destory 可用于在运行时移除游戏对象,或从游戏对象移除组件。

如果要销毁某个游戏对象,只需要引用与脚本关联的游戏对象。

void Update()
{
    if(Input.GetKey(KeyCode.Space))
    {
        Destroy(gameObject)
    }
}

但是问题在于,你可能会将这个脚本用于不同用途,因此不应该销毁对象,否则脚本组件也会随之被删除,因为二者是关联的。

public GameObject other;

void Update()
{
    if(Input.GetKey(KeyCode.Space))
    {
        Destroy(other);
    }
}

也可以使用 destroy 命令移除组件,而不是整个游戏对象。

void Update()
{
    if(Input.GetKey(KeyCode.Space))
    {
        Destroy(GetComponent<MeshRenderer>());
    }
}

上述例子都可以使用数字作为第2个参数,用来创建延时。

Destroy(gameObject, 3f);

GetButton 和 GetKey

在 Unity 中,GetKey 和 GetButton 通过 unity 的输入类接收按键或手柄按钮的输入。

两者的差异在于 GetKey 会使用 KeyCode 明确指定按键名称。

例如空格键,表示为 KeyCode.Space,这虽然适用于键盘按键,但建议使用 GetButton 指定你自己的控制。

输入管理器允许指定输入名称,然后给它指定一个键或按钮。要访问这个功能,可以从顶部菜单,编辑 -> project Settings -> Input。如需了解 Positive button 中可以输入哪些内容,可以参阅文档中的参考资料。

使用 GetKey 或者 GetButton 时,这些输入有3种状态,都会返回 true or false。

  • GetButtonDown 第一帧为true
  • GetButton
  • GetButtonUp 第一帧为true
void Update()
{
    bool down = Input.GetButtonDown("Jump");
    bool held = Input.GetButton("Jump");
    bool up = Input.GetButtonUp("Jump");

    if (down)
    {
        graphic.sprite = downgfx;
    }
    else if (held)
    {
        graphic.sprite = heldgfx;
    }
    else if (up)
    {
        graphic.sprite = upgfx;
    }
    else
    {
        graphic.sprite = standard;
    }
}

GetAxis

GetAxis 会返回浮点值,介于-1到1之间。轴在输入管理器中设置,从顶部菜单选择 Edit -> Project Settings -> Input -> Axes。对于轴,Positive Button 和 Negative Button 都要考虑,以及 Gravity, Sensitivity, Dead, Snap。

轴的 Gravity 会影响滑尺在按钮松开后归零的速度。Gravity 越高,归零速度越快。

Sensitivity 控制着输入的返回值到达1或-1的速度有多快,Sensitivity 值越大,反应速度就越快。

如果使用操纵杆表示轴,我们就不希望感受到操纵杆轻微移动的作用,为了避免这种情况,我们需要一个盲区。Dead 值越大,盲区越大。

Snap 选项的作用是,同时按下正负按钮时归零。

获取横轴或竖轴的值:

float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");

仅返回整数,不返回非整数

Input.GetAxis("Raw");

OnMouseDown

OnMouseDown 及其相关函数,可检测对碰撞体或GUI文本元素的点击。

public class MouseClick : MonoBehaviour
{
    private Rigidbody rb;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>()
    }

    void OnMouseDown()
    {
        rb.AddForce(-transform.forward * 500f);
        rb.useGravity = true;
    }
}

GetComponent

在 Unity 中,脚本被视为自定义组件,我们通常需要访问与同一个与游戏对象关联的其它脚本,甚至是其它游戏对象关联的脚本。

DeltaTime

delta 是指两个值之间的差。time 类的 DeltaTime 属性是指两次 Update 或 FixedUpdate 函数调用的间隔时长。它的作用是让用于移动其它增量计算的值变得平滑。

帧与帧之间的时差不是固定的,假设某个对象每帧移动固定距离,整体效果可能并不流畅,因为完成一帧所需的时间不同,虽然移动距离固定不变。

如果使用 Time.deltaTime 修改变化量,所需时间较长的帧,变化较大;所需时间较短的帧,变化较小。

public class UsingDeltaTime : MonoBehaviour
{
    public float speed = 8f;

    void Update()
    {
        if(Input.GetKey(KeyCode.RightArrow))
        {
            transform.position += new Vector3(speed * Time.deltaTime, 0.0f, 0.0f);
        }
    }
}

同时,我们可以控制立方体的移动,添加 speed 并由 Time.deltaTime 修改数量,移动变得流畅,即使帧率变化,速度也会保持恒定。

Time.deltaTime 的这种用法,让我们能更改每秒的值,而非每帧的值。

数据类型(DataTypes)

所有变量都会有数据类型。两种主要的数据类型为值类型(Value)和引用类型(Reference)。

Value

  • int
  • float
  • double
  • bool
  • char
  • Structs
    • Vector3
    • Quaternion

Reference

  • Classes
    • Transform
    • GameObject
void Start()
{
    Vector3 currentPosition = transform.position;
    // 因为 currentPosition 是值类型 Structs 的 Vector3
    // 所以只有 currentPosition 会受这行代码的影响
    // transform.position 不受影响
    currentPosition = new Vector3(0,2,0);
}
void Start()
{
    Transform tran = transform;
    // tran 是引用类型
    // transform 赋值给 tran 用的是赋值运算符
    // 所以 tran, transform 都指向同一个存储地址
    // 因此更改其中一个,另一个也会随之改变
    tran.position = new Vector3(0,2,0);
}

类(Classes)

    public class Inventory : MonoBehaviour
    {
        public class Stuff
        {
            public int projectileA;
            public int projectileB;
            public int projectileC;
            public float fuel;

            public Stuff(int prA, int prB, int prC)
            {
                projectileA = prA;
                projectileB = prB;
                projectileC = prC;
            }

            public Stuff(int prA, float fu)
            {
                projectileA = prA;
                fuel = fu;
            }

            public Stuff()
            {
                projectileA = 1;
                projectileB = 1;
                projectileC = 1;
            }
        }

        public Stuff myStuff = new Stuff(50, 5, 5);

        // 这个类的实例将用 第二个构造函数,因为参数匹配
        public Stuff myOtherStuff = new Stuff(50, 1.5f)

        void Start()
        {
            Debug.Log(myStuff.projectileA);
        }
    }

构造函数允许程序员设置默认值。

关于构造函数需要注意几点:

  • 构造函数的名称始终是类的名称。
  • 构造函数一定不会有返回类型,连 void 都不会有。
  • 一个类可能有多个不同的构造函数,但对象初始化时,只会调用其中一个构造函数。

Instantiate

Instantiate 函数的作用是克隆游戏对象。

它常用于克隆 prefab, prefab 就是指预配置对象,保存在项目素材中。

这类例子包括,从发射器发出的抛射体,每个抛射体都需要实例化到游戏世界中,从而实现发射操作。

public class UsingInstantiate : MonoBehaviour
{
    public Rigidbody projectile;
    public Transform barrelEnd;

    void Update()
    {
        if(Input.GetButtonDown("Fire1"))
        {   
            Rigidbody projectileInstance;
            projectileInstance = Instantiate(projectile, barrelEnd.position, barrelEnd.rotation) as Rigidbody;
            projectileInstance.AddForce(barrelEnd.up * 350f);
        }
    }
}

在游戏中创建多个克隆体时,这些克隆体仍会存在于场景中,所以,可能需要考虑编写一个脚本,在经过一段特定时间后,将它们从世界中移除。

如让 prefab 使用这个 ProjectileDestruction 脚本:

public class ProjectileDestruction : MonoBehaviour
{
    void Start()
    {
        Destroy(gameObject, 3.5f);
    }
}

数组

数组的作用是存储同类型数据。

int[] myIntArray = new int [5];

void Start()
{
    myIntArray[0] = 1;
    myIntArray[1] = 2;
    myIntArray[2] = 3;
    myIntArray[3] = 4;
    myIntArray[4] = 5;
}

// 也可以在一行中进行声明和初始化
int[] anotherArray = {1, 2, 3, 4, 5};

关于数组需要注意几点:

  • 设为公开数组,就能在 Inspector 中看到这个数组,并为它分配值。
public GameObject[] players;

void Start()
{
    players = GameObject.findGameObjectsWithTag("Player");

    for (int i = 0; i < players.Length; i++)
    {
        Debug.Log("Player Number " +i+ " is named " + players[i].name);
    }
}

Invoke

Invoke 函数的作用是将函数调用安排在指定延时后发生。

public class InvokeScript : MonoBehaviour
{
    public GameObject target;

    void Start()
    {   
        // 单位秒
        Invoke("SpawnObject", 2)
    }

    void SpawnObject()
    {
        Instantiate(target, new Vector3(0, 2, 0), Quaternion.identity);
    }
}

值的注意的是,只有不包含参数且返回类型为 void 的方法才能用 Invoke 调用。

InvokeRepeating 函数可以反复调用方法。

public GameObject target;

void Start()
{   
    // 参数1: 方法名字符串
    // 参数2: 调用方法前的延时,单位秒
    // 参数3: 调用间隔的延时,单位秒
    InvokeRepeating("SpawnObject", 2, 1);
}

void SpawnObject()
{
    float x = Random.Range(-2.0f, 2.0f);
    float z = Random.Range(-2.0f, 2.0f);
    Instantiate(target, new Vector3(x, 2, z), Quaternion.identity);
}

停止这个脚本中 Invoke 调用的所有实例,可以使用 CancelInvoke() 方法。

如果只想停止某个特定的 Invoke 可以传递方法名字符串,如:CancelInvoke("SpawnObject")

枚举

在 Unity 中编写脚本时,有时我们需要变量。

想象一下指南针的方位,我们可以使用整数描述方位,0代表北,1代表东,2代表南,3代表西。但是这种描述方式不易阅读,也不利于编写代码,因为这意味着需要记住每个数字代表的方位。

相反,我们可以创建名为枚举的变量,枚举通常被称为 enums。

enum Direction {North, East, South, West};

枚举中声明的每个常量,都有一个值,默认为从0开始往上数的整数。因此 North 的值为0,East 的值为1,South 的值为2,West 的值为3。

如果需要的话,值的类型和值本身都可以被覆盖。

// North: 1; East: 2; South: 3; West: 4
enum Direction {North = 1, East, South, West};

Switch 语句

public class ConversationScript : MonoBehaviour
{
    public int intelligence = 5;

    void Greet()
    {
        switch (intelligence)
        {
            case 5:
                print("Why hello there good sir! Let me teach you about Trigonometry!");
                break;
            case 4:
                print("Hello and good day!");
                break;
            case 3:
                print("Whadya want?");
                break;
            case 2:
                print("Grog SMASH!");
                break;
            case 1:
                print("Ulg, glib, Pblblblblb");
                break;
            default:
                print("Incorrect intelligence level.");
                break;
        }
    }
}