Unity三消游戏《水果乐园》源码深度解析:从算法到架构的完整实现
1. 项目概述与核心价值
拿到《水果乐园》这个Unity3D三消游戏完整源码,就像一位老厨师拿到了一份标注了所有火候、刀工和秘制酱料配方的招牌菜谱。对于想进入游戏开发领域,特别是对休闲益智类游戏感兴趣的朋友来说,这份源码的价值远超一个简单的可执行程序。它不是一个黑盒,而是一个完全透明、可供你反复拆解、学习和重构的“活体样本”。
三消游戏,听起来规则简单——三个或以上相同元素连成一线即可消除。但正是这种简单的规则背后,隐藏着一套复杂的状态管理、交互逻辑和反馈系统。从最基础的元素匹配检测,到消除后的空格填补、新元素生成,再到连锁反应(Combo)的判断、特殊道具的生成与效果,每一步都需要精密的算法和流畅的UI/UX配合。《水果乐园》源码为我们提供了一个绝佳的切入点,让我们能直观地看到这些抽象概念是如何被具象化为一行行代码、一个个预制体和一套套动画的。
这份源码适合谁?如果你是游戏开发新手,它将是你从“Hello World”迈向“可玩项目”的坚实桥梁;如果你是有一定经验的开发者,想快速切入三消赛道或借鉴其架构思路,它提供了完整的工程化范例;即便你只是对游戏设计感兴趣,通过阅读源码,你也能更深刻地理解那些让你“再来一局”的成瘾性设计是如何被一点点构建出来的。接下来,我们就深入这个“宝藏”,看看里面到底有哪些值得挖掘的“金矿”。
2. 源码整体架构与设计思路拆解
打开《水果乐园》的Unity工程,一个结构清晰的文件夹映入眼帘。这不仅仅是文件的堆砌,更是开发者思路的直观体现。一个优秀的三消游戏源码,其架构必然围绕着“数据驱动”和“状态分离”两大核心原则展开。
2.1 核心模块划分
通常,一个标准的三消游戏源码会包含以下几个核心模块,我在《水果乐园》中也一一找到了对应的实现:
游戏核心逻辑模块:这是游戏的大脑。主要负责棋盘(Grid)的数据管理、元素(Fruit Item)的生成与初始化、匹配(Match)检测算法(包括横向、纵向、L型、T型等)、消除(Clear)后的处理逻辑(如元素下落、新元素填充)、连锁反应计算等。这部分代码通常不直接依赖于Unity的MonoBehaviour,而是纯C#类,保证了逻辑的纯粹性和可测试性。
用户交互模块:这是游戏的双手。负责处理玩家的输入,主要是鼠标或触摸的拖拽、点击。它需要精确判断玩家选中的是哪个元素,并处理元素交换(Swap)的请求。这个模块是连接玩家意图与游戏核心逻辑的桥梁,需要处理丰富的交互状态,如“等待输入”、“交换中”、“交换无效回退”等。
视图表现模块:这是游戏的脸面。将核心逻辑模块中的抽象数据(如“第3行第4列是草莓”)转化为屏幕上生动的精灵(Sprite)或模型。它管理着所有游戏元素的视觉表现、交换动画、消除特效、下落动画、分数飘字等。这部分严重依赖Unity的UGUI或SpriteRenderer、Animator等组件。
关卡与数据管理模块:这是游戏的规则书。定义了关卡目标(例如:在30步内获得2000分、消除20个苹果等)、初始棋盘布局、特殊障碍物(如冰块、铁链、果冻)的位置。数据通常以ScriptableObject或JSON/XML配置文件的形式存在,实现数据与代码的解耦,方便策划人员调整。
特殊元素与道具系统:这是游戏的调味剂。实现了如“直线消除”、“爆炸范围消除”、“彩虹万能元素”等特殊水果,以及“刷新棋盘”、“提示”等主动使用道具。它们的实现涉及更复杂的匹配规则和消除效果,是提升游戏深度和策略性的关键。
《水果乐园》的源码在模块划分上做得比较清晰,例如,你能找到一个GameManager或BoardManager类来统筹全局,一个Grid类来管理棋盘数据,Fruit类代表每个水果元素,InputHandler类处理交互,以及一系列LevelData的ScriptableObject。这种结构让你在阅读时能快速定位功能点,而不是在数千行代码中迷失。
2.2 设计模式的应用
在阅读源码时,我特别注意了其中设计模式的应用,这是区分“能运行”的代码和“好维护”的代码的关键。
- 状态模式:游戏必然有多个状态,如
Ready(准备)、Playing(游戏中)、Swapping(交换中)、Matching(匹配检测中)、Filling(填充中)、GameOver(结束)。《水果乐园》很可能使用一个枚举(Enum)来标记当前游戏状态,并在Update循环或协程(Coroutine)中根据状态执行不同的逻辑。更优雅的实现可能会用一个GameStateMachine(状态机)来管理,让状态切换和对应行为更加清晰。 - 观察者模式:这是游戏开发中最常用的模式之一。当某个水果被消除时,需要通知计分系统加分、通知任务系统更新进度、播放音效、触发粒子特效。通过C#的事件(
event)和委托(delegate),或者Unity的UnityEvent,可以实现这种松耦合的通知机制。在源码中,你可能会看到类似OnFruitCleared、OnScoreChanged这样的事件定义。 - 对象池模式:三消游戏中,水果元素不断地被消除和生成。如果每次都
Instantiate(实例化)和Destroy(销毁),会产生大量的GC(垃圾回收)开销,导致游戏卡顿。一个成熟的源码必然会包含一个FruitPool,预先创建好一定数量的水果对象,使用时激活,消除后回收到池中待用,极大地提升了性能。
理解这些设计思路,比你单纯抄写代码更有价值。它让你知道为什么代码要这样组织,未来当你需要添加一个新功能(比如“每日任务”)时,你知道该在哪个模块下手,如何与现有系统优雅地集成。
3. 核心算法与逻辑深度解析
三消游戏的灵魂在于其核心算法。光看运行效果可能觉得顺理成章,但背后每一帧的计算都充满了智慧。《水果乐园》源码为我们提供了这些算法的具体实现,我们来逐一拆解。
3.1 棋盘数据结构的表示
棋盘是游戏的基础。在代码中,棋盘通常用一个二维数组Fruit[,] grid来表示,grid[x, y]就代表了棋盘第x列、第y行位置上的水果对象(可能为null,代表空格)。有些实现为了更直观,会用grid[col, row]。这里的一个关键细节是坐标原点的选择:是左下角为(0,0)还是左上角为(0,0)?这会影响后续所有关于位置计算的逻辑。在Unity的2D坐标系中,世界坐标原点在中心,而网格逻辑原点常设在左下角。源码中必须保持逻辑坐标与视觉坐标转换的一致性。
3.2 匹配检测算法详解
这是三消游戏最核心的算法。当玩家交换两个元素后,系统需要快速检测整个棋盘是否存在可消除的组合。常见的算法是扫描法。
- 横向扫描:遍历棋盘的每一行。用一个
List<Fruit>临时记录当前连续的同种水果。从左到右,如果下一个水果与当前记录的水果类型相同,就加入列表;如果不同,则判断当前列表长度是否>=3,如果是,则将这些水果标记为“待消除”,然后清空列表,开始记录新的水果类型。 - 纵向扫描:同理,遍历每一列,从上到下或从下到上检测垂直方向的连续相同水果。
在《水果乐园》中,这个检测函数可能叫做FindAllMatches()或CheckForMatches()。它返回一个包含所有被匹配水果的列表。这里有几个高级技巧:
- L型、T型等特殊形状:它们本质上是横向和纵向匹配的组合。一个水果可能同时属于一个横向匹配组和一个纵向匹配组,在合并匹配列表时需要去重。
- 匹配后立即检测:在消除一批水果后,上方水果下落,可能形成新的匹配(即连锁反应)。因此,消除和填充的流程必须循环执行,直到某次检测后不再产生新的匹配为止。这个循环通常用
while循环或递归来实现。
// 伪代码示例:核心匹配检测循环 List<Fruit> GetAllMatchesToClear() { List<Fruit> allMatches = new List<Fruit>(); List<Fruit> horizontalMatches = FindHorizontalMatches(); List<Fruit> verticalMatches = FindVerticalMatches(); allMatches.AddRange(horizontalMatches); // 添加纵向匹配时需去重,防止L/T型中的水果被重复计算 foreach(var fruit in verticalMatches) { if(!allMatches.Contains(fruit)) allMatches.Add(fruit); } return allMatches; } IEnumerator ProcessMatches() { List<Fruit> matches; do { // 等待交换动画完成 yield return new WaitForSeconds(swapDuration); // 检测匹配 matches = GetAllMatchesToClear(); if(matches.Count > 0) { // 执行消除、得分、特效 ClearFruits(matches); // 等待消除特效 yield return new WaitForSeconds(clearDuration); // 执行下落和新元素填充 yield return StartCoroutine(FallAndFillRoutine()); // 填充后,继续循环检测是否有新的匹配产生 } } while (matches.Count > 0); // 直到没有新匹配产生 }3.3 消除填充与下落逻辑
消除之后,棋盘会出现空洞。标准的处理方式是:
- 下落:对于每一列,从下往上遍历。遇到空洞时,将其上方的所有非空元素依次向下移动一格。这个过程需要伴随视觉上的下落动画。
- 填充:每一列顶部的空洞,需要生成新的随机水果来填充。新水果通常从棋盘上方“掉落”进来。
这里的一个性能优化点是:不要为每个下落的水果单独启动一个协程或动画。这会导致大量的协程开销和不可控的完成时间。更好的做法是,统一计算每个水果的起始位置和目标位置,然后在一个统一的协程中,用Vector3.Lerp或DOTween插件对所有水果进行插值移动,并等待所有移动完成。这在《水果乐园》的源码中应该能看到类似FallFruits()这样的函数,它管理着整列或整盘的下落过程。
注意:新生成的水果在填充后,必须立即进行一次全局的匹配检测,以防“天降好运”直接形成新的消除。这就是上面
do...while循环的意义。
3.4 特殊元素与道具的实现逻辑
特殊元素(如炸弹、彩虹糖)极大地丰富了游戏性。它们的实现本质上是对基础匹配检测和消除逻辑的扩展。
- 直线消除(条纹糖):通常在形成4个一行的匹配时生成。它本身可以作为一个特殊类型的水果保存在
grid中。当它被消除(无论是参与匹配还是被引爆)时,其效果不是消除自身,而是触发一个额外的逻辑:消除它所在整行或整列的所有普通水果。在代码中,这会调用一个ClearEntireRow(int row)或ClearEntireColumn(int col)的方法。 - 范围爆炸(包装糖):通常在形成正方形(2x2)匹配时生成。被引爆时,消除以自身为中心3x3范围内的所有水果。实现上是一个双循环遍历周围格子。
- 彩虹糖(万能元素):这是最特殊的。它通常由5个或以上的匹配生成。它的逻辑不是“被消除时做什么”,而是“它参与交换时做什么”。当彩虹糖与一个普通水果交换时,它会消除棋盘上所有与该普通水果同类型的水果。这里的实现关键在于,交换操作本身会触发匹配检测,但彩虹糖的交换需要被特殊处理:先不执行普通交换,而是触发彩虹糖的“全局消除”技能,然后再处理后续下落。
在源码中,这些特殊元素通常会继承自一个基础的Fruit类,并重写其OnCleared()或OnSwapped()方法。道具系统(如“重排棋盘”、“额外步数”)则更像是全局技能,由GameManager直接调用,操作整个grid数据。
4. 关键代码模块与Unity组件实战
看懂了设计思路和算法,我们再来看看在Unity工程中,这些逻辑是如何与具体的组件和场景结合起来的。这是从理论到实践的关键一步。
4.1 GameManager:游戏的总指挥
GameManager通常是一个单例(Singleton),贯穿整个游戏生命周期。在《水果乐园》中,它可能负责:
- 加载关卡数据:根据当前关卡ID,从
Resources文件夹或AssetBundle中加载对应的LevelDataScriptableObject,获取棋盘大小、目标分数、障碍物布局等信息。 - 初始化棋盘:调用
BoardManager或Grid类,根据关卡数据生成初始的grid数组,并实例化视觉上的水果预制体。 - 管理游戏流程:控制游戏状态的切换(Ready -> Playing -> GameOver),管理倒计时、步数。
- 协调各系统:当分数更新时,它通知UI管理器更新分数显示;当关卡目标达成时,它弹出胜利界面。
- 持久化数据:使用
PlayerPrefs或更专业的存档系统,保存玩家的金币、解锁的关卡、道具数量等。
它的Update方法或一系列协程,是驱动整个游戏循环的主引擎。
4.2 Fruit预制体与组件设计
在Project面板中,你会找到名为Fruit或Fruit_Prefab的资源。点开它,一个典型的配置可能是:
- SpriteRenderer:用于显示水果的图片。
- Animator(可选):控制水果的选中、交换、消除等动画状态。
- BoxCollider2D:用于接收鼠标或触摸事件。
- Fruit (Script):这是我们自己编写的核心脚本,挂载在这个预制体上。
这个Fruit脚本里有什么?
public FruitType type;:一个枚举,定义它是苹果、香蕉还是草莓。public int gridX, gridY;:记录它在逻辑棋盘grid中的坐标。public bool isMatched;:一个标志位,在匹配检测时被标记为true。- 一些方法,如
Initialize(FruitType type, int x, int y)用于初始化,OnMouseDown()或通过事件监听来处理点击。
一个重要的技巧是:视觉与逻辑的绑定。当逻辑上的grid[2,3]需要生成一个视觉上的水果时,代码会实例化Fruit预制体,然后调用其Initialize方法,将逻辑坐标(2,3)和水果类型传递给它。同时,这个水果对象的引用也会被赋给grid[2,3]。这样,无论是通过逻辑坐标找到视觉对象,还是通过点击视觉对象反查逻辑坐标,都能快速实现。
4.3 输入处理与交换动画
交互的流畅度直接决定游戏手感。在InputHandler脚本中,通常会监听OnMouseDown、OnMouseEnter(用于拖拽)和OnMouseUp事件。
- 选中:玩家点击一个水果,脚本记录下这个水果为
firstSelectedFruit,并可能播放一个“放大”或“高亮”的动画。 - 拖拽:如果玩家拖动鼠标到相邻的另一个水果上,这个水果会被记录为
secondSelectedFruit。 - 交换:在
OnMouseUp时,判断firstSelectedFruit和secondSelectedFruit是否相邻(通过比较gridX和gridY的差值绝对值之和是否为1)。如果相邻,则调用GameManager或BoardManager的TrySwap方法。
TrySwap方法是关键:
- 它首先在逻辑
grid数组中交换两个水果的位置数据。 - 然后,命令两个水果的视觉对象播放交换动画(例如,用
DOTween.To让它们的Transform.position在0.3秒内移动到对方的位置)。 - 重要:在交换动画进行期间,游戏状态应设为
Swapping,并阻塞玩家输入。 - 动画结束后,立即调用匹配检测。如果检测到有效匹配,则进入消除流程;如果没有匹配,则必须执行“回退”操作:再次交换两个水果的逻辑数据和视觉位置,回到交换前的状态,并给玩家一个“无效交换”的视觉反馈(比如抖动一下)。这个“回退”动画同样需要时间,并且在此期间也要阻塞输入。
// 伪代码示例:尝试交换 public IEnumerator TrySwap(Fruit fruitA, Fruit fruitB) { if(currentState != GameState.Playing) yield break; currentState = GameState.Swapping; // 1. 逻辑交换 SwapGridPositions(fruitA, fruitB); // 2. 视觉交换动画 yield return StartCoroutine(PlaySwapAnimation(fruitA, fruitB)); // 3. 检测匹配 List<Fruit> matches = GetAllMatchesToClear(); if(matches.Count == 0) { // 无效交换,回退 yield return StartCoroutine(PlaySwapAnimation(fruitA, fruitB)); // 再交换回来 SwapGridPositions(fruitA, fruitB); // 逻辑也换回来 // 可以播放一个抖动动画提示无效 ShakeBoard(); } else { // 有效交换,处理消除 yield return StartCoroutine(ProcessMatches()); } currentState = GameState.Playing; }4.4 使用DOTween优化动画体验
在《水果乐园》这类游戏中,流畅的动画至关重要。原生的Coroutine配合Lerp可以实现动画,但代码会显得繁琐。源码中很可能使用了流行的DOTween插件来简化动画创作。
例如,下落动画可以这样写:
// 不使用DOTween IEnumerator FallToPosition(Vector3 targetPos, float duration) { Vector3 startPos = transform.position; float elapsed = 0; while(elapsed < duration) { transform.position = Vector3.Lerp(startPos, targetPos, elapsed / duration); elapsed += Time.deltaTime; yield return null; } transform.position = targetPos; } // 使用DOTween Sequence fallSequence = DOTween.Sequence(); foreach(Fruit fruit in fruitsNeedToFall) { Vector3 targetPos = CalculateWorldPosition(fruit.gridX, fruit.gridY); fallSequence.Join(fruit.transform.DOMove(targetPos, 0.5f).SetEase(Ease.OutBounce)); } fallSequence.OnComplete(()=>{ /* 所有下落完成后的回调 */ });DOTween的Sequence(序列)可以方便地组织多个同时或顺序发生的动画,SetEase可以设置丰富的缓动函数,让动画更有弹性、更自然。在消除特效、分数飘字、UI弹窗等方面,DOTween都能大显身手。阅读源码时,可以关注using DG.Tweening的命名空间和相关的DO前缀方法。
5. 性能优化与常见问题排查
即使有了完整的源码,直接运行或修改后也可能遇到性能问题和各种Bug。这部分分享一些我在实际开发三消游戏时积累的“避坑”经验。
5.1 性能瓶颈分析与优化
三消游戏在低端移动设备上容易卡顿,主要瓶颈来自两方面:CPU计算和Draw Call。
CPU优化:
- 匹配检测的优化:不要在每一帧都进行全盘扫描。只在必要的时候触发:交换后、填充后。并且,可以优化扫描算法。例如,交换只可能影响交换点周围有限区域内的匹配情况,可以只检测局部区域,而不是整个8x8的大棋盘。
- 避免在Update中做复杂计算:像寻路、大量的集合操作(如
List.Contains在数据量大时较慢)不要放在Update中。对于连锁消除的循环检测,要确保有终止条件,防止意外死循环。 - 使用对象池:前面提到过,这是必须的。不仅仅是水果预制体,连消除特效的粒子系统、得分飘字的Text组件,都应该使用对象池管理。
GPU优化(减少Draw Call):
- 合批:Unity会对使用相同材质和纹理的静态物体进行静态合批,对动态物体进行动态合批。确保所有同种类水果的SpriteRenderer使用同一个图集(Sprite Atlas)中的精灵,这样它们大概率能被合批,显著降低Draw Call。在《水果乐园》的源码中,检查
Sprite Atlas的配置是关键。 - 避免UI Overdraw:复杂的、半透明的UI叠加会加重GPU负担。优化UI层级,减少不必要的全屏遮罩。
- 合批:Unity会对使用相同材质和纹理的静态物体进行静态合批,对动态物体进行动态合批。确保所有同种类水果的SpriteRenderer使用同一个图集(Sprite Atlas)中的精灵,这样它们大概率能被合批,显著降低Draw Call。在《水果乐园》的源码中,检查
5.2 常见Bug与调试技巧
在研读或修改源码时,你可能会遇到以下典型问题:
交换后无反应或直接回退:
- 检查匹配检测算法:首先在交换后打印
grid数组,看逻辑交换是否成功。然后单步调试GetAllMatchesToClear函数,看它是否正确地识别出了匹配。常见错误是坐标计算错误,或者匹配条件判断有误(比如要求严格大于3,而不是大于等于3)。 - 检查游戏状态:确保在播放交换动画时,
currentState确实被设为了Swapping或Busy,并且在动画完成前,InputHandler的输入被正确屏蔽。
- 检查匹配检测算法:首先在交换后打印
元素下落错乱或堆叠:
- 检查下落目标位置计算:
CalculateWorldPosition(int x, int y)这个函数是核心。确保它计算的坐标与棋盘原点、格子间距、元素锚点对齐。一个快速调试方法是在Start时,在每一个格子位置实例化一个小方块作为参考点。 - 检查下落顺序:下落必须从每列的最底部的空洞开始处理,还是从顶部开始?通常是从下往上扫描,将上方元素下落填充下方空洞。如果顺序错了,会导致元素“穿过”空洞或堆叠。
- 检查下落目标位置计算:
特殊元素效果不触发:
- 检查继承与多态:确保特殊水果类(如
StripedFruit)正确重写了基类Fruit的OnCleared方法。在清除时,是调用的fruit.OnCleared()还是直接将其从grid中移除?如果是后者,多态就不会生效。 - 检查触发条件:彩虹糖的触发条件是“交换时”,而不是“被匹配时”。确认代码是在交换判断的逻辑分支里调用了彩虹糖的特殊方法。
- 检查继承与多态:确保特殊水果类(如
关卡数据加载失败:
- 检查Resources路径:如果使用
Resources.Load<LevelData>("Levels/Level_1"),请确保LevelData资产确实放在Assets/Resources/Levels/文件夹下。Unity对路径大小写敏感。 - 检查ScriptableObject创建:确认
LevelData是一个有效的ScriptableObject,并且其公共字段(如boardWidth,boardHeight)已在Inspector中正确赋值。
- 检查Resources路径:如果使用
5.3 扩展功能的设计思路
当你吃透了《水果乐园》的基础源码后,可能会想添加新功能。这里提供几个方向:
- 添加新障碍物:如“冰块”,需要被匹配相邻两次才能消除。可以在
Fruit类上增加一个int iceLayer属性,每次匹配到相邻时iceLayer--,当iceLayer<=0时,才真正执行清除逻辑。视觉上,可以用一个子对象来显示冰层Sprite,并随iceLayer减少而改变。 - 实现关卡目标系统:除了分数,可以增加“收集特定水果”、“打破所有冰块”等目标。为
LevelData增加一个List<LevelGoal>字段。在GameManager中,每当有水果被消除,不仅加分,还遍历所有目标,检查是否更新进度。 - 设计更复杂的特殊元素:例如“彩色炸弹”,与任何水果交换,会随机将棋盘上半数水果变成另一种随机类型。这需要扩展你的特殊效果系统,可能涉及遍历整个
grid并修改FruitType。
6. 从源码学习到独立开发的进阶路径
《水果乐园》源码是一座金矿,但挖矿之后,如何冶炼出属于自己的金子?以下是我建议的学习和进阶路径:
第一步:通读与运行。不要急着改代码,先让项目在Unity Editor里完美地跑起来。点击每一个按钮,玩通几个关卡,用Visual Studio或Rider的调试器附加到Unity进程,设几个断点,跟踪一次完整的“点击-交换-匹配-消除-下落”流程,观察每个变量的变化。这是建立感性认识。
第二步:局部修改与实验。尝试做一些小的、安全的修改来验证你的理解:
- 把棋盘从8x8改成6x6或10x10。你需要改哪些地方?(
LevelData、Grid初始化、世界坐标计算...) - 修改一种水果的图片。你需要换哪个图集里的精灵?
- 把匹配要求从3个改成4个。修改
GetAllMatchesToClear函数中的常数。 - 给消除增加一个“爆炸”音效。在哪里触发这个音效最合适?(
Fruit.OnCleared方法里,或者GameManager的清除函数里)。
第三步:重写核心模块。这是最具挑战性也收获最大的一步。新建一个场景,不要导入原来的GameManager、Grid等脚本。仅参考《水果乐园》的设计思路和算法,自己从零开始实现:
- 创建一个空的棋盘。
- 实现随机初始化填充(确保无初始匹配)。
- 实现鼠标拖拽交换。
- 实现匹配检测与消除。
- 实现下落与填充。
- 实现连锁反应。
在这个过程中,你会遇到原源码已经解决但你未曾留意的问题,比如“如何防止初始棋盘出现匹配?”(这需要在你随机生成后,调用一次匹配检测,如果发现匹配,则重新生成或交换其中几个元素)。通过自己动手解决,这些知识才真正属于你。
第四步:架构优化与扩展。对比你的实现和《水果乐园》源码的实现,思考:
- 谁的代码更清晰?为什么?
- 谁的性能可能更好?有没有可以借鉴的设计模式?
- 如果你想加一个“撤销上一步”功能,两种架构下,哪个更容易实现?
最终,你可以基于《水果乐园》的核心理念,融入你自己的创意,开发一个全新的三消游戏。比如,把水果换成宝石,加入RPG元素,让消除能为角色积累能量释放技能;或者做成一个叙事驱动的三消,每个关卡解锁一段故事。
这份源码最大的价值,在于它为你展示了一条完整的、被验证可行的开发路径。它省去了你从零摸索基础框架的巨大时间成本,让你能把精力集中在更富创造性的游戏设计和玩法创新上。记住,最好的学习不是复制,而是在理解的基础上,进行批判性的思考和实践性的改造。