在Unity中玩转表达式树:解锁游戏逻辑的动态魔法


在Unity中玩转表达式树:解锁游戏逻辑的动态魔法

在Unity 2021 LTS版本中,结合Burst Compiler可以将表达式树编译后的委托性能提升至接近原生C++代码水平,特别适合高频调用的游戏系统(如物理伤害计算、AI决策等)

参考:git-amend

Expression

一、为什么要学习表达式树?

传统Unity开发面临三大痛点:

  1. 逻辑固化 - 编译后无法修改行为逻辑
  2. 组件强耦合 - GameObject之间依赖关系复杂
  3. 动态性不足 - 难以实现运行时逻辑热替换

表达式树(Expression Trees)技术通过将代码转换为可操作的数据结构,完美解决了这些问题。它允许我们:

  • 运行时动态构建逻辑运行时动态构建逻辑
  • 实现组件间的弱耦合通信实现组件间的弱耦合通信
  • 支持可视化配置游戏行为支持可视化配置游戏行为

二、核心应用场景

  1. 动态技能系统

  2. 数据驱动AI

    • 通过JSON配置行为树
    • 运行时解析并生成表达式
    • 实现无需重新编译的AI逻辑更新
  3. MOD支持系统

    • 玩家自定义逻辑脚本
    • 安全沙箱运行表达式
    • 实时加载玩家创作内容

三、实战演示

一、 属性获取器

传统模式缺陷:

 
 
 
xxxxxxxxxx
 
 
 
 
public int GetPlayerStat(Player p, string statName)
{
    switch(statName)
    {
        case "Health": return p.Health;
        case "Mana": return p.Mana;
        // 每新增一个属性需要修改此处
    }
}
 

表达式树:

 
 
 
xxxxxxxxxx
 
 
 
 
using System;
using System.Linq.Expressions;
using UnityEngine;

public class ExpressionTreeDemo : MonoBehaviour {
    void Start() {
        Player player = new () { Health = 100 };
        Func<Player, int> healthProperty = CreatePropertyGetter<Player, int>("Health");
        Debug.Log($"Player Health: {healthProperty(player)}");
    }

    public int GetPlayerStat(Player player, string statName) {
        Func<Player, int> propertyGetter = CreatePropertyGetter<Player, int>(statName);
        return propertyGetter(player);
    }

    public Func<T, TProperty> CreatePropertyGetter<T, TProperty>(string propertyName) {
        ParameterExpression param = Expression.Parameter(typeof(T), "x");
        MemberExpression property = Expression.Property(param, propertyName);
        Expression<Func<T, TProperty>> lambda = Expression.Lambda<Func<T, TProperty>>(property, param);
        return lambda.Compile();
    }
}
 

应用场景:获取对象属性 技术要点

  1. 属性访问表达式属性访问表达式:Expression.Property

二、条件触发系统

 
 
 
xxxxxxxxxx
 
 
 
 
public class ConditionTrigger : MonoBehaviour 
{
    public string ConditionExpression = "Player.Health.CurrentHP < 0.3";
    public GameObject ContextObject;

    private Func<GameObject, bool> _compiledCondition;
    private static Dictionary<string, Func<GameObject, bool>> _cache = new();

    void Start()
    {
        if (!_cache.TryGetValue(ConditionExpression, out _compiledCondition))
        {
            var elements = ConditionExpression.Split('.');
            var rootObj = Expression.Parameter(typeof(GameObject), "context");
            Expression accessChain = rootObj;
            foreach (var element in elements.Skip(1))
            {
                accessChain = Expression.PropertyOrField(accessChain, element);
            }

            var conditionExpr = BuildComparison(accessChain, "<", Expression.Constant(0.3f));
            _compiledCondition = Expression.Lambda<Func<GameObject, bool>>(conditionExpr, rootObj).Compile();
            _cache[ConditionExpression] = _compiledCondition;
        }
    }

    void Update()
    {
        if (_compiledCondition(ContextObject))
        {
            Debug.Log("触发条件!");
        }
    }

    private Expression BuildComparison(Expression left, string operatorStr, Expression right)
    {
        return operatorStr switch
        {
            "<" => Expression.LessThan(left, right),
            ">" => Expression.GreaterThan(left, right),
            "==" => Expression.Equal(left, right),
            "!=" => Expression.NotEqual(left, right),
            "<=" => Expression.LessThanOrEqual(left, right),
            ">=" => Expression.GreaterThanOrEqual(left, right),
            _ => throw new NotSupportedException($"不支持的运算符: {operatorStr}")
        };
    }
}
 

应用场景:动态游戏事件触发 优势对比

传统方式表达式树方案
硬编码条件判断 支持运行时修改条件逻辑
需要预定义所有情况 可通过配置文件动态加载

三、行为链组合

 
 
 
xxxxxxxxxx
 
 
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using UnityEngine;

// 使用示例
public class ComboExample : MonoBehaviour
{
    private ComboSystem _comboSystem;

    void Start()
    {
        _comboSystem = new ComboSystem();

        // 添加连招动作
        _comboSystem.ActionExpressions.Add(GetComboExpression(typeof(AttackAnimation), nameof(AttackAnimation.Play), Expression.Constant("SwordSwing")));
        _comboSystem.ActionExpressions.Add(GetComboExpression(typeof(EffectsManager), nameof(EffectsManager.Spawn), Expression.Constant("SwordHit")));
        _comboSystem.ActionExpressions.Add(GetComboExpression(typeof(AttackAnimation), nameof(AttackAnimation.Play), Expression.Constant("SwordSwing2")));
        _comboSystem.ActionExpressions.Add(GetComboExpression(typeof(DamageCalculator), nameof(DamageCalculator.Apply), Expression.Constant(new Vector3(0, 1, 0)), Expression.Constant(100f)));

        // 执行连招
        _comboSystem.ExecuteCombo();
    }

    Expression<Action> GetComboExpression(Type type, string methodName, params Expression[] args)
    {
        return Expression.Lambda<Action>(Expression.Call(type, methodName, null, args));
    }
}

public class ComboSystem
{
    // 存储动作表达式的列表
    public List<Expression<Action>> ActionExpressions = new();

    // 执行连招
    public void ExecuteCombo()
    {
        // 构建复合表达式
        var comboBlock = Expression.Block(
            ActionExpressions.Select(exp => exp.Body)
        );

        // 创建最终的表达式
        //Compile() 方法将 Lambda 表达式编译为可执行的委托。
        //Invoke() 方法调用该委托,从而执行块中的所有表达式。
        var finalExpr = Expression.Lambda<Action>(comboBlock);
        finalExpr.Compile().Invoke(); // 执行连招
    }
}

// 示例动作类
public class AttackAnimation
{
    public static void Play(string animationName)
    {
        Debug.Log($"播放动画: {animationName}");
    }
}

public class EffectsManager
{
    public static void Spawn(string effectName)
    {
        Debug.Log($"生成特效: {effectName}");
    }
}

public class DamageCalculator
{
    public static void Apply(Vector3 position, float damage)
    {
        Debug.Log($"应用伤害: {damage} 到位置: {position}");
    }
}

//生成特效: SwordHit
//播放动画: SwordSwing2
//应用伤害: 100 到位置: (0.00, 1.00, 0.00)
 

Expression.Block:

Expression.Block 是一个非常强大的功能,它允许我们将多个表达式组合成一个块(block),并在执行时按顺序执行这些表达式。

 

四、运行时状态机

 
 
 
xxxxxxxxxx
 
 
 
 
using System;
using System.Linq.Expressions;
using System.Reflection;
using UnityEngine;
using Object = UnityEngine.Object;

public class EnemyStateMachine : MonoBehaviour {
    
    //定义状态机委托,以Enemy和Hero为参数,返回一个以Enemy和Hero为参数的空方法
    //它会根据英雄的状态(如生命值和距离)返回一个相应的行为函数。
    Func<Enemy, Hero, Action<Enemy, Hero>> stateEvaluator;
    //存储当前行为函数
    Action<Enemy, Hero> behavior;
    Enemy enemy;
    Hero hero;

    void Start() {
        enemy = FindObjectOfType<Enemy>();
        hero = FindObjectOfType<Hero>();
        stateEvaluator = CreateDynamicStateMachine();
    }

    void Update() {
        //获取当前行为
        behavior = stateEvaluator(enemy, hero);
        //执行当前行为
        behavior(enemy, hero);
        
        Debug.Log("Enemy Aggression Level:", enemy.AggressionLevel);
    }

    public Func<Enemy, Hero, Action<Enemy, Hero>> CreateDynamicStateMachine() {
        //定义参数表达式
        ParameterExpression enemyParam = Expression.Parameter(typeof(Enemy), "enemy");
        ParameterExpression heroParam = Expression.Parameter(typeof(Hero), "hero");
        
        //定义一个二元表达式
        BinaryExpression heroLowHealth = Expression.LessThan(
            Expression.Property(heroParam, "Health"),
            Expression.Constant(30)
        );
        BinaryExpression heroNear = Expression.LessThan(
            Expression.Property(heroParam, "Distance"),
            Expression.Constant(10f)
        );
        
        Debug.Log($"HeroLowHealth{heroLowHealth}");
        Debug.Log($"HeroNear{heroNear}");
        
        var attack = CreateActionExpression("Attack").Compile();
        var taunt = CreateActionExpression("Taunt").Compile();
        var patrol = CreateActionExpression("Patrol").Compile();
        
        //条件表达式,如果heroNear为真则执行taunt,否则执行patrol
        ConditionalExpression tauntOrPatrol = Expression.Condition(heroNear, Expression.Constant(taunt), Expression.Constant(patrol));
        ConditionalExpression finalCondition = Expression.Condition(heroLowHealth, Expression.Constant(attack), tauntOrPatrol);
        
        //
        var lambda = Expression.Lambda<Func<Enemy, Hero, Action<Enemy, Hero>>>(finalCondition, enemyParam, heroParam);
        return lambda.Compile();
    }
    
    Expression<Action<Enemy, Hero>> CreateActionExpression(string methodName) {
        ParameterExpression enemyParam = Expression.Parameter(typeof(Enemy), "enemy");
        ParameterExpression heroParam = Expression.Parameter(typeof(Hero), "hero");
        
        MethodInfo method = typeof(Enemy).GetMethod(methodName, new[] { typeof(Hero) });
        
        MethodCallExpression call = Expression.Call(enemyParam, method, heroParam);
        return Expression.Lambda<Action<Enemy, Hero>>(call, enemyParam, heroParam);
    }
}
 

CreateDynamicStateMachine

  • 参数定义:定义了两个参数 enemyParamheroParam,用于表示敌人和英雄。

  • 条件表达式:

  • heroLowHealth: 检查英雄的生命值是否低于 30。

    • heroNear: 检查英雄与敌人的距离是否小于 10。
  • 行为选择

  • 使用 CreateActionExpression 方法创建 AttackTauntPatrol 行为的表达式。

    • 使用 Expression.Condition 创建条件表达式,根据条件选择行为。
  • 返回 Lambda 表达式:最终返回一个 Lambda 表达式,接受 EnemyHero 作为参数,并返回相应的行为函数。

CreateActionExpression

  • 参数定义:定义 enemyParamheroParam
  • 获取方法信息:使用反射获取 Enemy 类中与 methodName 相对应的方法。
  • 创建方法调用表达式:使用 Expression.Call 创建方法调用表达式,并返回一个 Lambda 表达式。
posted @   世纪末の魔术师  阅读(154)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
历史上的今天:
2023-02-22 Unity下简易字符串指令调试
  1. 1 ありがとう··· KOKIA
ありがとう··· - KOKIA
00:00 / 00:00
An audio error has occurred.

作词 : KOKIA

作曲 : KOKIA

编曲 : 日向敏文

作词 : KOKIA

作曲 : KOKIA

誰もが気付かぬうちに

誰もが気付かぬうちに

何かを失っている

フッと気付けばあなたはいない

思い出だけを残して

せわしい時の中

言葉を失った人形達のように

街角に溢れたノラネコのように

声にならない叫びが聞こえてくる

もしも もう一度あなたに会えるなら

もしも もう一度あなたに会えるなら

たった一言伝えたい

ありがとう

ありがとう

時には傷つけあっても

時には傷つけあっても

あなたを感じていたい

思い出はせめてもの慰め

いつまでもあなたはここにいる

もしも もう一度あなたに会えるなら

もしも もう一度あなたに会えるなら

たった一言伝えたい

ありがとう

ありがとう

もしも もう一度あなたに会えるなら

もしも もう一度あなたに会えるなら

たった一言伝えたい

もしも もう一度あなたに会えるなら

たった一言伝えたい

ありがとう

ありがとう

時には傷つけあっても

時には傷つけあっても

あなたを感じてたい

点击右上角即可分享
微信分享提示