• 【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,全部脚本如下:

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class RubyController : MonoBehaviour
    5. {
    6. //设置最大生命值(生命上限)
    7. public int maxHealth = 5;
    8. //当前生命值,默认为0
    9. private int currentHealth;
    10. private void Start()
    11. {
    12. //初始化当前生命值
    13. currentHealth = maxHealth;
    14. }
    15. // 每帧都会执行一次Update函数
    16. void Update()
    17. {
    18. ...
    19. }
    20. //更改生命值
    21. //amount是游戏中加血/减血的操作
    22. void ChangeHealth(int amount)
    23. {
    24. //限制当前生命值范围为[0,maxHealth]
    25. currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
    26. //输出
    27. Debug.Log("当前生命值为:" + currentHealth + "/" + maxHealth);
    28. }
    29. }

    1.2 触发器脚本

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

    edd43227e9314f249a574d771e3f0a04.png

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

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

     的游戏情景。

    脚本具体如下

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class HealthCollectible : MonoBehaviour
    5. {
    6. //设置每次碰撞加的血量
    7. public int amount = 1;
    8. private int CollideTimes;
    9. //添加触发器事件,每次碰撞触发器时执行其中的代码
    10. //其中,在这里对于草莓来说,other指的就是玩家Ruby
    11. private void OnTriggerEnter2D(Collider2D other)
    12. {
    13. CollideTimes++;//碰撞次数+1
    14. Debug.Log($"和当前物体发生碰撞的是:{other},当前是第{CollideTimes}次发生碰撞!");
    15. //获取Ruby对象的脚本组件,还是用GetComponent方法
    16. RubyController rubyController=other.GetComponent();
    17. if(rubyController != null)
    18. {
    19. //血量+1
    20. rubyController.ChangeHealth(amount);
    21. //销毁血包
    22. Destroy(gameObject);
    23. }
    24. else
    25. {
    26. Debug.LogError("未获取到当前游戏对象");
    27. }
    28. }
    29. }

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

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

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

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

    运行效果

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

    89006ec29daa4beabe7425ce96dd7142.png

    2.2 使用internal修饰符

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

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

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

    对应的修改成:

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

    运行效果

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

    15647198f3cd4fba90ffeb7f44275c50.png

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

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

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

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

    • RubyController
    1. //设置最大生命值(生命上限)
    2. public int maxHealth = 5;
    3. //当前生命值,默认为0
    4. public int currentHealth;
    5. //C#中支持面向对象程序设计中的封装,实现对数据成员进行保护
    6. //数据成员变量本身是私有的,只能通过某一种方法或者属性访问
    7. //属性是共有的,可以通过取值器--get,赋值器--set,设定对应字段的访问规则
    8. //满足规则才能访问该成员变量
    9. public int health { get { return currentHealth; } }

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

    • HealthCollectible
    1. if(rubyController != null)
    2. {
    3. //血量+1
    4. rubyController.ChangeHealth(amount);
    5. if (rubyController.health
    6. {
    7. //销毁血包
    8. Destroy(gameObject);
    9. }
    10. }

    该方法优化点在于

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

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

    3 C#中get和set访问器

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

    3.1 定义属性 

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

    1. public 数据类型 属性名称
    2. {
    3. get
    4. {
    5. 获取属性的语句块;
    6. return 值;
    7. }
    8. set
    9. {
    10. 设置属性得到语句块;
    11. }
    12. }

    get{} 取值器

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

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

    set{} 赋值器

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

    3.2 自动实现的属性

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

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

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

    例如:

    1. public class Goods //商品类
    2. {
    3. public string Name//商品类的名称,自动实现的属性
    4. {
    5. getset
    6. }
    7. public decimal Price
    8. {
    9. getset
    10. }
    11. }

    3.3 结合案例

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

    1. public int health
    2. {
    3. get
    4. {
    5. return currentHealth;
    6. }
    7. }

    如果需要set值,可以是:

    1. public int health
    2. {
    3. get
    4. {
    5. return currentHealth;
    6. }
    7. set
    8. {
    9. currentHealth = 1;
    10. }
    11. }

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

     

  • 相关阅读:
    找工作必备!如何让面试官对你刮目相看,建议收藏尝试!!
    vue首页加载优化,vue使用cdn加载插件,vue cdn优化
    时序预测 | MATLAB实现NGO-GRU北方苍鹰算法优化门控循环单元时间序列预测
    读 | SA : The Hard Parts 之数据所有权
    2.4G无线麦克风领夹麦一拖二_全双工_杰理JL6976M单芯片方案
    说说TIME_WAIT和CLOSE_WAIT区别
    运维需要懂产品和运营吗?
    [CISCN2019 华北赛区 Day1 Web5]CyberPunk
    我的设计模式之旅 ⑦ 观察者模式
    MySQL进阶4,常见函数
  • 原文地址:https://blog.csdn.net/qq_41835314/article/details/126212570