【Unity入门计划】Unity实例-C#如何通过封装实现对数据成员的保护

目录

1 实例概述

1.1 玩家脚本

1.2 触发器脚本

2 通过当前血量判断是否销毁血包

2.1 将private修改成public实现跨类调用

运行效果

2.2 使用internal修饰符

运行效果

2.3 将血量封装起来,定义属性访问

该方法优化点在于

3 C#中get和set访问器

3.1 定义属性 

get{} 取值器

set{} 赋值器

3.2 自动实现的属性

3.3 结合案例

 


学习的教程

【unity2021入门教程】68-2D游戏开发教程系列-03-RubyAdventure2DRpg官方教程-16-完善触发器代码_哔哩哔哩_bilibili

为什么会写这么一篇博客呢?在跟着教程学习制作RubyAdventure项目的过程中,进行到了“吃血包加血”的脚本编写,其中涉及到需要在“血包”类脚本中调用“玩家Ruby”脚本里的成员变量的操作,其中在进行脚本优化时就涉及到了封装、对数据成员保护的概念,正好有一个参考案例,就打算记录一下C#封装的实操。

1 实例概述

需要实现玩家Ruby在场景中移动,吃掉“血包”草莓并加血的操作。

1.1 玩家脚本

当前项目中玩家挂了一个脚本,里面写入了赋予的最大生命值maxHealth和当前生命值currentHealth,全部脚本如下:

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

public class RubyController : MonoBehaviour
{

    //设置最大生命值(生命上限)
    public int maxHealth = 5;
    //当前生命值,默认为0
    private int currentHealth;
    private void Start()
    {
        //初始化当前生命值
        currentHealth = maxHealth;
    }
    // 每帧都会执行一次Update函数
    void Update()
    {
        ...
    }

    //更改生命值
    //amount是游戏中加血/减血的操作
    void ChangeHealth(int amount)
    {
        //限制当前生命值范围为[0,maxHealth]
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
        //输出
        Debug.Log("当前生命值为:" + currentHealth + "/" + maxHealth);
    }
}

1.2 触发器脚本

在游戏场景中,我们加入了一个游戏对象CollectibleHealth(“血包”,以下简称草莓),挂了一个2D碰撞体组件,勾选Is Trigger当作触发器使用。

【Unity入门计划】Unity实例-C#如何通过封装实现对数据成员的保护_第1张图片

给草莓挂一个脚本,通过添加碰撞检测后调用的OnTriggerEnter()内容实现

  • 血包被玩家碰到后被吃掉
  • 玩家血量增加1
  • Debug返回指定语句

 的游戏情景。

脚本具体如下

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

public class HealthCollectible : MonoBehaviour
{
    //设置每次碰撞加的血量
    public int amount = 1;
    private int CollideTimes;
    //添加触发器事件,每次碰撞触发器时执行其中的代码
    //其中,在这里对于草莓来说,other指的就是玩家Ruby
    private void OnTriggerEnter2D(Collider2D other)
    {
        CollideTimes++;//碰撞次数+1
        Debug.Log($"和当前物体发生碰撞的是:{other},当前是第{CollideTimes}次发生碰撞!");

        //获取Ruby对象的脚本组件,还是用GetComponent方法
        RubyController rubyController=other.GetComponent();
        if(rubyController != null)
        {
            //血量+1
            rubyController.ChangeHealth(amount);
            //销毁血包
            Destroy(gameObject);
        }
        else
        {
            Debug.LogError("未获取到当前游戏对象");
        }
    }
}

2 通过当前血量判断是否销毁血包

2.1 将private修改成public实现跨类调用

由于在Ruby脚本中,当前血量currentHealth的修饰符是private,为了实现跨类调用,选择直接把currentHealth暴露出来,变成public,就可以调用了,于是代码分别做以下修改:

  • RubyController类
    //设置最大生命值(生命上限)
    public int maxHealth = 5;
    //当前生命值,默认为0
    //将生命值暴露出来,private -> public
    public int currentHealth;
  • HealthCollectible类
        if(rubyController != null)
        {
            //血量+1
            rubyController.ChangeHealth(amount);
            if(rubyController.currentHealth < rubyController.maxHealth)
            {
                //销毁血包
                Destroy(gameObject);
            }
        }
        else
        {
            Debug.LogError("未获取到当前游戏对象");
        }

运行效果

在项目中,currentHealth这个本应该是内部的变量被暴露在了项目属性栏中。

【Unity入门计划】Unity实例-C#如何通过封装实现对数据成员的保护_第2张图片

2.2 使用internal修饰符

与public相比,用internal修饰符的成员变量可以在同一应用程序集内部访问。而public可以跨程序集访问。

同一程序集中,二者效果相同。

同时,在Unity中,public会将变量同时暴露在inspector属性栏中,internal则不会。

对应的修改成:

  •  RubyController类
    //设置最大生命值(生命上限)
    public int maxHealth = 5;
    //当前生命值,默认为0
    //private -> internal
    internal int currentHealth;
  •  HealthCollectible类
        if(rubyController != null)
        {
            //血量+1
            rubyController.ChangeHealth(amount);
            if(rubyController.currentHealth < rubyController.maxHealth)
            {
                //销毁血包
                Destroy(gameObject);
            }
        }
        else
        {
            Debug.LogError("未获取到当前游戏对象");
        }

运行效果

可以看到,属性栏中是不会将currentHealth展示出来的。

15647198f3cd4fba90ffeb7f44275c50.png

但是,我认为无论是internal还是public,都是十分不安全的!这给了任何人可以随意修改内部值——玩家血量的机会,不利于项目的维护。

2.3 将血量封装起来,定义属性访问

这里就涉及到一个面向对象程序设计的概念——封装,对于封装,我的另一篇博客有简单的介绍,这里就不再赘述:【Unity入门计划】了解C#或Unity中的类和对象_flashinggg的博客-CSDN博客

直接介绍方法,同样还是修改两个类:

  • RubyController
    //设置最大生命值(生命上限)
    public int maxHealth = 5;

    //当前生命值,默认为0
    public int currentHealth;

    //C#中支持面向对象程序设计中的封装,实现对数据成员进行保护
    //数据成员变量本身是私有的,只能通过某一种方法或者属性访问
    //属性是共有的,可以通过取值器--get,赋值器--set,设定对应字段的访问规则
    //满足规则才能访问该成员变量
    public int health { get { return currentHealth; } }

这里用到了get——取值器,后面会详细介绍C#的访问器。

  • HealthCollectible
        if(rubyController != null)
        {
            //血量+1
            rubyController.ChangeHealth(amount);
            if (rubyController.health

该方法优化点在于

需要访问的数据成员类型仍旧是私有的,只是在类中定义一个属性用于访问,但数据成员变量本身是私有的,从而实现了对数据成员的保护

这点是十分重要的!对于一些内部的变量,不要轻易地设置成public!

3 C#中get和set访问器

C# get和set访问器:获取和设置字段(属性)的值 (biancheng.net)

3.1 定义属性 

游戏对象的属性常与字段连用.C#提供了get访问器和set访问器,方便获取和设置字段的值,定义属性的语法框架如下:

public 数据类型 属性名称
{
    get
    {
        获取属性的语句块;
        return 值;
    }
    set
    {
        设置属性得到语句块;
    }
}

get{} 取值器

get作为取值器,用于获取属性的值,需要在get语句最后使用return关键字返回获取属性的关系值。

如果在属性定义中省略了get{}访问器,则无法再该类外的其他类获取私有类型的字段值,这时也被称为只写属性

set{} 赋值器

set{}作为赋值器,用于给对应字段设置值,这里需要用到一个特殊的值value,这个value就是给当前字段设置的值。

3.2 自动实现的属性

本小节叙述来自于:C#——get方法和set方法(属性) - 简书 (jianshu.com)

在某些情况下,get和set访问器仅向支持字段赋值或仅从其中检索值,不包括任何逻辑,通过使用自动实现的属性,既能够简化代码,还能让C#编译器透明地提供支持字段。

如果属性具有get和set访问器,则必须自动实现这两个访问器。自动实现的属性通过以下方式定义:使用get和set关键字,但不提供任何实现。

例如:

public class Goods  //商品类
{
      public string Name//商品类的名称,自动实现的属性
       {
          get; set;
 }
       public decimal Price
      { 
          get; set;
}
}

3.3 结合案例

上述例子中就提到了,定义一个int类型的属性,属性名为health,用了get{}访问器获取了私有类型currentHealth的字段值,并用关键字return返回。

public int health 
{
    get
    { 
        return currentHealth; 
    }
}

如果需要set值,可以是:

public int health 
{
    get
    { 
        return currentHealth; 
    }
    set
    {
        currentHealth = 1;
    }
}

但由于上述案例中,currentHealth本身不需要被赋值,不然就跟public差不多了,所以这里没有set。

 

你可能感兴趣的:(Unity学习,unity,游戏,c#)