侧边栏壁纸
博主头像
komi

Bona Fides

  • 累计撰写 16 篇文章
  • 累计创建 32 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

看看Unturned在反编译后的模样

komi
2025-07-23 / 0 评论 / 0 点赞 / 17 阅读 / 3,360 字

自打17年左右开始玩Unturned就一直好奇作为《未转变者》(Unturned)的独立开发者Nelson当初是如何打造这款沙盒游戏的 自己在github上乱逛发现了这个 直接逆向工程了 也能看到用的是自己写的反编译工具 (用到了Assembly-CSharp.dll) 虽然经过了Nelson本人的许可 也打算在近年公开Unturned的部分源码 但是反编译这种行为在大多数软件的EULA协议应该是明令禁止的 既然Nelson本人加上SDG都许可了 What Can I Say?
挑了几个之前比较好奇的游戏机制看看实现原理

姜丝(zombie)的攻击方式

https://github.com/Unturned-Datamining/Unturned-Datamining/blob/linux-client-preview/Assembly-CSharp/SDG.Unturned/Zombie.cs

    private enum EAbilityChoice
    {
        ThrowBoulder, // 扔巨石
        SpitAcid, // 吐酸
        Stomp, // 重踏地面产生冲击
        BreatheFire, // 吐火
        ElectricShock, // 电击
        Flashbang // 闪光
    }

扔石头

    public void askThrow()
    {
        if (!isDead)
        {
            lastSpecial = Time.time;
            isThrowingBoulder = true;
            isPlayingBoulder = true;
            if (!Dedicator.IsDedicatedServer)
            {
                animator.Play("Boulder_0");
                AudioClip clip = ZombieManager.roars[UnityEngine.Random.Range(0, 16)];
                OneShotAudioParameters oneShotAudioParameters = new OneShotAudioParameters(base.transform, clip);
                oneShotAudioParameters.volume = 0.5f;
                oneShotAudioParameters.pitch = GetRandomPitch();
                oneShotAudioParameters.SetLinearRolloff(1f, 32f);
                oneShotAudioParameters.Play();
            }
            boulderItem = ((GameObject)UnityEngine.Object.Instantiate(Resources.Load("Characters/Mega_Boulder_Item"))).transform;
            boulderItem.name = "Boulder";
            boulderItem.parent = rightHook;
            boulderItem.localPosition = Vector3.zero;
            boulderItem.localRotation = Quaternion.Euler(0f, 0f, 90f);
            boulderItem.localScale = Vector3.one;
            UnityEngine.Object.Destroy(boulderItem.gameObject, 2f);
        }
    }

    private void StopThrowingBoulder()
    {
        if (isThrowingBoulder)
        {
            isThrowingBoulder = false;
            if (boulderItem != null)
            {
                UnityEngine.Object.Destroy(boulderItem.gameObject);
            }
            if (Provider.isServer)
            {
                seeker.canMove = true;
            }
        }
    }

    public void askBoulder(Vector3 origin, Vector3 direction)
    {
        if (!isDead)
        {
            Transform obj = ((GameObject)UnityEngine.Object.Instantiate(Resources.Load(Dedicator.IsDedicatedServer ? "Characters/Mega_Boulder_Projectile_Server" : "Characters/Mega_Boulder_Projectile_Client"))).transform;
            obj.name = "Boulder";
            EffectManager.RegisterDebris(obj.gameObject);
            obj.position = origin;
            obj.rotation = Quaternion.LookRotation(direction) * Quaternion.Euler((float)UnityEngine.Random.Range(0, 2) * 180f, (float)UnityEngine.Random.Range(0, 2) * 180f, (float)UnityEngine.Random.Range(0, 2) * 180f);
            obj.localScale = Vector3.one * 1.75f;
            obj.GetComponent<Rigidbody>().AddForce(direction * 1500f);
            obj.GetComponent<Rigidbody>().AddRelativeTorque(UnityEngine.Random.Range(-500f, 500f), UnityEngine.Random.Range(-500f, 500f), UnityEngine.Random.Range(-500f, 500f), ForceMode.Force);
            obj.Find("Trap").gameObject.AddComponent<Boulder>();
            UnityEngine.Object.Destroy(obj.gameObject, 8f);
        }
    }

askThrow是在准备巨石 在非服务端添加些随机的僵尸音效 巨石本身的一些状态初始化 加载素材、相对僵尸的位置(rightHook)、石头本身的大小和旋转角度

而真正投掷的逻辑在askBoulder 在确认僵尸未死亡的情况下绘制投掷物(巨石) 注册巨石为碎片类 方便管理清除、巨石投掷前随机的旋转、石头变大(1.75倍)、 给它一个力的大小和方向(AddForce)、巨石投掷期间自身的滚动(AddRelativeTorque)

最后是StopThrowingBoulder 停止投掷巨石的状态 确保巨石类被销毁、让僵尸寻路机制恢复(能动)

寻路机制

    public void tick()
    {
       // 判断是否在刚刚reset后(重置)的状态
        if (needsTickForPlacement)
        {
            needsTickForPlacement = false;  // 去除临时标志位
            setTicking(wantsToTick: false);  // 取消计时
            GetComponent<CharacterController>().Move(Vector3.down);  // 保证接触地面
            return;
        }
        float num = Time.time - lastTick;  // 上次计时流逝秒数
        lastTick = Time.time;  // 更新最近计时时间
        lastPull = Time.time;  // 更新最近吸引时间
        // 判断是否昏厥状态
        if (isStunned)
        {
            return;
        }
        undergroundTestTimer -= num;  // (这里undergroundTestTimer最初设置的是10) 检查流逝秒数
        // 如果定时器触发情况下对僵尸进行是否越界判断
        if (undergroundTestTimer < 0f)
        {
            undergroundTestTimer = UnityEngine.Random.Range(30f, 60f);  // 重新设置更大点的检查秒数间隔
            // 越界判断
            if (!UndergroundAllowlist.IsPositionWithinValidHeight(base.transform.position))
            {
                ZombieManager.teleportZombieBackIntoMap(this);  // 重新传送回地图内
                return;
            }
        }
        // 狩猎目标为玩家时
        if (huntType == EHuntType.PLAYER)
        {
        	// 若最近玩家不存在时 停止寻路
            if (player == null)
            {
                stop();
                return;
            }
        }
        // 狩猎目标为固定地图点、当前处于未移动状态、距上次狩猎时间至现在超出3秒时 停止寻路
        else if (huntType == EHuntType.POINT && !isMoving && Time.time - lastHunted > 3f)
        {
            stop();
            return;
        }
        // 当前存在狩猎玩家
        if (player != null)
        {
	    // 玩家已经挂了的情况下 慢速撤离
            if (player.life.isDead)
            {
                leave(quick: false);
                return;
            }
            // 在玩家超出导航识别范围或者玩家在水中游泳且僵尸不在水中 快速撤离
            if (player.movement.nav == byte.MaxValue || (player.stance.stance == EPlayerStance.SWIM && !WaterUtility.isPointUnderwater(base.transform.position)))
            {
                leave(quick: true);
                return;
            }
        }
        // 识别阻挡车辆 若阻挡车辆失效(打爆炸了) 清除标识
        if (targetObstructionVehicle != null && targetObstructionVehicle.isDead)
        {
            targetObstructionVehicle = null;
        }
        // 识别玩家正在驾驶车辆 若车辆失效(离开/打爆了) 清除标识
        if (targetPassengerVehicle != null && targetPassengerVehicle.isDead)
        {
            targetPassengerVehicle = null;
        }
        // 识别是否有可攻击的可交互物体 若物体失效(无生命值) 清除标识
        if (targetObject != null && targetObject.isAllDead())
        {
            targetObject = null;
        }
        // 卡滞状态检测 大概每0.25秒对环境检测一遍 看看有没有可攻击的目标
        if (isStuck)
        {
            float num2 = Time.time - lastStuck;  // 计算距上次卡住时间秒数
            if (num2 > 0.75f)  // 短时间卡滞
            {
                stuckSearchTimer += num;  // 卡滞累计时间
                if (stuckSearchTimer > 0.25f)
                {
                    stuckSearchTimer = 0f; // 计时清零
                    // 重新寻找可攻击目标
                    if (targetBarricade == null && targetStructure == null && targetObstructionVehicle == null && targetPassengerVehicle == null && targetObject == null)
                    {
                        findTargetWhileStuck();
                    }
                }
            }
            else
            {
                stuckSearchTimer = 0f;
            }
            // 长时间卡滞且没有攻击 状态清零 若在当前区域有尸潮召唤器可直接传送
            if (num2 > 5f && zombieRegion.hasBeacon && Time.time - lastAttack > 10f)
            {
                lastStuck = Time.time;
                stuckSearchTimer = 0f;
                ZombieManager.teleportZombieBackIntoMap(this);
                return;
            }
        }
        // ...

这里的tick方法是在ZombieManager中的Update方法调用的 相当于是游戏实时计算会用到的状态检测功能 后面的代码属实是长… 可以看到Unity开发游戏绝对不是什么轻松事

That hurts!

https://github.com/Unturned-Datamining/Unturned-Datamining/blob/linux-client-preview/Assembly-CSharp/SDG.Unturned/PlayerLife.cs


    public void askDamage(byte amount, Vector3 newRagdoll, EDeathCause newCause, ELimb newLimb, CSteamID newKiller, out EPlayerKill kill)
    {
        askDamage(amount, newRagdoll, newCause, newLimb, newKiller, out kill, trackKill: false, ERagdollEffect.NONE, canCauseBleeding: true);
    }

    public void askDamage(byte amount, Vector3 newRagdoll, EDeathCause newCause, ELimb newLimb, CSteamID newKiller, out EPlayerKill kill, bool trackKill = false, ERagdollEffect newRagdollEffect = ERagdollEffect.NONE)
    {
        askDamage(amount, newRagdoll, newCause, newLimb, newKiller, out kill, trackKill, newRagdollEffect, canCauseBleeding: true);
    }

    public void askDamage(byte amount, Vector3 newRagdoll, EDeathCause newCause, ELimb newLimb, CSteamID newKiller, out EPlayerKill kill, bool trackKill = false, ERagdollEffect newRagdollEffect = ERagdollEffect.NONE, bool canCauseBleeding = true)
    {
        askDamage(amount, newRagdoll, newCause, newLimb, newKiller, out kill, trackKill, newRagdollEffect, canCauseBleeding, bypassSafezone: false);
    }

    /// <param name="bypassSafezone">Should damage be dealt even while inside safezone?</param>
    public void askDamage(byte amount, Vector3 newRagdoll, EDeathCause newCause, ELimb newLimb, CSteamID newKiller, out EPlayerKill kill, bool trackKill = false, ERagdollEffect newRagdollEffect = ERagdollEffect.NONE, bool canCauseBleeding = true, bool bypassSafezone = false)
    {
        kill = EPlayerKill.NONE; 
        if (bypassSafezone || InternalCanDamage())
        {
            doDamage(amount, newRagdoll, newCause, newLimb, newKiller, out kill, trackKill, newRagdollEffect, canCauseBleeding);
        }
    }

    private void doDamage(byte amount, Vector3 newRagdoll, EDeathCause newCause, ELimb newLimb, CSteamID newKiller, out EPlayerKill kill, bool trackKill = false, ERagdollEffect newRagdollEffect = ERagdollEffect.NONE, bool canCauseBleeding = true)
    {
        kill = EPlayerKill.NONE;    // 初始化攻击来源
        // 判断攻击点数是否无效或者玩家已死亡
        if (amount == 0 || isDead || !IsAlive)
        {
            return;
        }
        // 致死情况
        if (amount >= health)
        {
            _health = 0;
        }
        // 减血量
        else
        {
            _health -= amount;
        }
        ragdoll = newRagdoll; // 布娃娃肢体效果
        ragdollEffect = newRagdollEffect;  // 布娃娃肢体材质
        // 非致命伤且数值大于3阈值后触发镜头抖动
        if (_health > 0 && amount > 3)
        {
            SendDamagedEvent.Invoke(GetNetId(), ENetReliability.Reliable, base.channel.GetOwnerTransportConnection(), amount, newRagdoll.normalized);
        }
        // 生命值更新 镜头血腥颜色渐变
        SendHealth.Invoke(GetNetId(), ENetReliability.Reliable, base.channel.GetOwnerTransportConnection(), health);
        // 没太清楚这里的功能 貌似项目中没有找到定义的地方...
        OnTellHealth_Global?.Invoke(this);
        // PVP打架的逻辑
        if (newCause == EDeathCause.GUN || newCause == EDeathCause.MELEE || newCause == EDeathCause.PUNCH || newCause == EDeathCause.ROADKILL || newCause == EDeathCause.GRENADE || newCause == EDeathCause.MISSILE || newCause == EDeathCause.CHARGE)
        {
            recentKiller = newKiller;
            lastTimeTookDamage = Time.realtimeSinceStartup;
            Player player = PlayerTool.getPlayer(recentKiller);
            if (player != null)
            {
                // 记录最近攻击时间
                player.life.lastTimeCausedDamage = Time.realtimeSinceStartup;
                // 处理好战标记逻辑
                // 冷却期内直接标记
                if (Time.realtimeSinceStartup - player.life.lastTimeAggressive < COMBAT_COOLDOWN)
                {
                    player.life.markAggressive(force: true);
                }
                // 潜在攻击 这里的逻辑其实有些看的云里雾里...
                else if ((player.life.recentKiller == CSteamID.Nil || Time.realtimeSinceStartup - player.life.lastTimeTookDamage > COMBAT_COOLDOWN) && Time.realtimeSinceStartup - lastTimeCausedDamage > COMBAT_COOLDOWN)
                {
                    player.life.markAggressive(force: true);
                }
            }
        }
        if (health == 0)
        {
            // 死亡延时奖励
            try
            {
                base.player.quests.InterruptDelayedQuestRewards(EDelayedQuestRewardsInterruption.Death);
            }
            catch (Exception e)
            {
                UnturnedLog.exception(e, "Caught exception interrupting delayed quest rewards on death:");
            }
            if (recentKiller != CSteamID.Nil && recentKiller != base.channel.owner.playerID.steamID && Time.realtimeSinceStartup - lastTimeTookDamage < COMBAT_COOLDOWN)
            {
                // 击杀玩家声望机制(Hero/Bandit)
                Player player2 = PlayerTool.getPlayer(recentKiller);
                if (player2 != null)
                {
                    // 根据玩家是否好战决定声望赏罚 最小1点 最大25点
                    int value = Mathf.Abs(base.player.skills.reputation);
                    value = Mathf.Clamp(value, 1, 25);
                    if (player2.life.isAggressor)
                    {
                        value = -value;
                    }
                    player2.skills.askRep(value);
                }
            }
            // 记录击杀类型为玩家
            kill = EPlayerKill.PLAYER;
            // 判断是不是PVP打架导致死亡
            wasPvPDeath = newCause == EDeathCause.GUN || newCause == EDeathCause.MELEE || newCause == EDeathCause.PUNCH || newCause == EDeathCause.ROADKILL || newCause == EDeathCause.GRENADE || newCause == EDeathCause.MISSILE || newCause == EDeathCause.CHARGE || newCause == EDeathCause.SENTRY;
            PlayerLife.OnPreDeath.TryInvoke("OnPreDeath", this); // 死亡预处理 貌似也没找到对应的方法...
            base.player.movement.forceRemoveFromVehicle(); // 强制从驾驶车辆移除
            RocketLegacyOnDeath.TryInvoke("RocketLegacyOnDeath", this, newCause, newLimb, newKiller); // 可能和Rocket插件定义的内容相关
            try
            {
                // 死亡统计
                SendDeath.Invoke(GetNetId(), ENetReliability.Reliable, base.channel.GetOwnerTransportConnection(), newCause, newLimb, newKiller);
                // 死亡效果
                SendDead.InvokeAndLoopback(GetNetId(), ENetReliability.Reliable, Provider.GatherRemoteClientConnections(), ragdoll, ragdollEffect);
            }
            catch (Exception e2)
            {
                UnturnedLog.warn("Exception during tellDeath or tellDead:");
                UnturnedLog.exception(e2);
            }
            // 加载新的生成点
            if (spawnpoint == null || (newCause != EDeathCause.SUICIDE && newCause != EDeathCause.BREATH) || Time.realtimeSinceStartup - lastSuicide > 60f)
            {
                spawnpoint = LevelPlayers.getSpawn(isAlt: false);
            }
            // 更新最近自杀时间
            if (newCause == EDeathCause.SUICIDE || newCause == EDeathCause.BREATH)
            {
                lastSuicide = Time.realtimeSinceStartup;
            }
            // 击杀任务统计
            if (trackKill)
            {
                // 遍历当前服务器上的所有玩家
                for (int i = 0; i < Provider.clients.Count; i++)
                {
                    SteamPlayer steamPlayer = Provider.clients[i];
                    // 需要当前玩家处于有效状态后再进行统计
                    if (!(steamPlayer.player == null) && !(steamPlayer.player.movement == null) && !(steamPlayer.player.life == null) && !steamPlayer.player.life.isDead && steamPlayer != base.channel.owner && (steamPlayer.player.transform.position - base.transform.position).sqrMagnitude < 90000f)
                    {
                        steamPlayer.player.quests.trackPlayerKill(base.player);
                    }
                }
            }
            // 死亡信息广播(有可能用插件做一些酷炫的提示?)
            broadcastPlayerDied(this, newCause, newLimb, newKiller);
            // 死亡原因窗口显示
            if (CommandWindow.shouldLogDeaths)
            {
                switch (newCause)
                {
                case EDeathCause.BLEEDING:
                    CommandWindow.Log(Provider.localization.format("Bleeding", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.BONES:
                    CommandWindow.Log(Provider.localization.format("Bones", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.FREEZING:
                    CommandWindow.Log(Provider.localization.format("Freezing", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.BURNING:
                    CommandWindow.Log(Provider.localization.format("Burning", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.FOOD:
                    CommandWindow.Log(Provider.localization.format("Food", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.WATER:
                    CommandWindow.Log(Provider.localization.format("Water", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.GUN:
                case EDeathCause.MELEE:
                case EDeathCause.PUNCH:
                case EDeathCause.ROADKILL:
                case EDeathCause.GRENADE:
                case EDeathCause.MISSILE:
                case EDeathCause.CHARGE:
                case EDeathCause.SPLASH:
                {
                    SteamPlayer steamPlayer2 = PlayerTool.getSteamPlayer(newKiller);
                    string text;
                    string text2;
                    if (steamPlayer2 != null)
                    {
                        text = steamPlayer2.playerID.characterName;
                        text2 = steamPlayer2.playerID.playerName;
                    }
                    else
                    {
                        text = "?";
                        text2 = "?";
                    }
                    string text3 = "";
                    switch (newLimb)
                    {
                    case ELimb.LEFT_FOOT:
                    case ELimb.LEFT_LEG:
                    case ELimb.RIGHT_FOOT:
                    case ELimb.RIGHT_LEG:
                        text3 = Provider.localization.format("Leg");
                        break;
                    case ELimb.LEFT_HAND:
                    case ELimb.LEFT_ARM:
                    case ELimb.RIGHT_HAND:
                    case ELimb.RIGHT_ARM:
                        text3 = Provider.localization.format("Arm");
                        break;
                    case ELimb.SPINE:
                        text3 = Provider.localization.format("Spine");
                        break;
                    case ELimb.SKULL:
                        text3 = Provider.localization.format("Skull");
                        break;
                    }
                    switch (newCause)
                    {
                    case EDeathCause.GUN:
                        CommandWindow.Log(Provider.localization.format("Gun", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text3, text, text2));
                        break;
                    case EDeathCause.MELEE:
                        CommandWindow.Log(Provider.localization.format("Melee", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text3, text, text2));
                        break;
                    case EDeathCause.PUNCH:
                        CommandWindow.Log(Provider.localization.format("Punch", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text3, text, text2));
                        break;
                    case EDeathCause.ROADKILL:
                        CommandWindow.Log(Provider.localization.format("Roadkill", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text, text2));
                        break;
                    case EDeathCause.GRENADE:
                        CommandWindow.Log(Provider.localization.format("Grenade", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text, text2));
                        break;
                    case EDeathCause.MISSILE:
                        CommandWindow.Log(Provider.localization.format("Missile", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text, text2));
                        break;
                    case EDeathCause.CHARGE:
                        CommandWindow.Log(Provider.localization.format("Charge", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text, text2));
                        break;
                    case EDeathCause.SPLASH:
                        CommandWindow.Log(Provider.localization.format("Splash", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName, text, text2));
                        break;
                    }
                    break;
                }
                case EDeathCause.ZOMBIE:
                    CommandWindow.Log(Provider.localization.format("Zombie", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.ANIMAL:
                    CommandWindow.Log(Provider.localization.format("Animal", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.SUICIDE:
                    CommandWindow.Log(Provider.localization.format("Suicide", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.INFECTION:
                    CommandWindow.Log(Provider.localization.format("Infection", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                case EDeathCause.BREATH:
                    CommandWindow.Log(Provider.localization.format("Breath", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                    break;
                default:
                    switch (newCause)
                    {
                    case EDeathCause.ZOMBIE:
                        CommandWindow.Log(Provider.localization.format("Zombie", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.VEHICLE:
                        CommandWindow.Log(Provider.localization.format("Vehicle", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.SHRED:
                        CommandWindow.Log(Provider.localization.format("Shred", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.LANDMINE:
                        CommandWindow.Log(Provider.localization.format("Landmine", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.ARENA:
                        CommandWindow.Log(Provider.localization.format("Arena", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.SENTRY:
                        CommandWindow.Log(Provider.localization.format("Sentry", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.ACID:
                        CommandWindow.Log(Provider.localization.format("Acid", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.BOULDER:
                        CommandWindow.Log(Provider.localization.format("Boulder", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.BURNER:
                        CommandWindow.Log(Provider.localization.format("Burner", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.SPIT:
                        CommandWindow.Log(Provider.localization.format("Spit", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    case EDeathCause.SPARK:
                        CommandWindow.Log(Provider.localization.format("Spark", base.channel.owner.playerID.characterName, base.channel.owner.playerID.playerName));
                        break;
                    }
                    break;
                }
            }
        }
        // 流血状态同步
        else if (Provider.modeConfigData.Players.Can_Start_Bleeding && canCauseBleeding && amount >= 20)
        {
            serverSetBleeding(newBleeding: true);
        }
        // 受伤效果(可能是插件内定义)
        this.onHurt?.Invoke(base.player, amount, newRagdoll, newCause, newLimb, newKiller);
    }

这个方法是玩家受伤的核心机制 基本上包括了状态检测、死亡判断、声望累计、流血效果、任击杀务统计、击杀条件广播等

最后

项目的代码设计的实在是复杂(说明这是Nelson迭代了很多次后的方案)加上我对Unity本身不是很懂行 所以没多摘代码来看
回头看 自17年到现在这个我已经玩了242个小时的游戏经反编译后 没想到这个不起眼的沙盘游戏竟是这样一个Monolith 不过有句话说得好 “罗马建成非一日之功” 如果不是Nelson对游戏开发的热情和SDG的大力支持 Unturned也不会像现在这样 这是值得我去学习的

0

评论区