[Unity Demo]从零开始制作空洞骑士Hollow Knight第十五集:制作更多地图,更多敌人,更多可交互对象

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、第一个代表性场景
    • 1.制作更多敌人
    • 2.制作更多可交互对象
  • 二、第二个代表性场景
    • 1.制作更多敌人
    • 2.制作更多可交互对象
  • 三、第三个代表性场景
    • 1.制作更多敌人
    • 2.制作更多可交互对象
  • 总结


前言

hello大家好久没见,之所以隔了这么久才更新并不是因为我又放弃了这个项目,而是接下来要制作的工作太忙碌了,每次我都花了很长的时间解决完一个部分,然后就没力气打开CSDN写文章就直接睡觉去了,现在终于有时间整理下我这半个月都做了什么内容

废话少说,接下来我将介绍我做的几个代表性场景,主要是如标题说的,制作更多地图,更多敌人,更多可交互对象

另外,我的Github已经更新了,想要查看最新的内容话请到我的Github主页下载工程吧:

GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!


一、第一个代表性场景

1.制作更多敌人

我们先把制作好创建吧,还是老规矩先用tk2dTilemap绘制好基础的地图样貌并添加上Collider:

然后堆叠素材添加上去,这样一个场景就做好了:

首先当然是从Town跳下井里的第一个场景Crossroads_01,这里我们设置Town到Crossroads_01的TransitionPoint为top2:

然后回到Crossroads中我们设置好全部的TransitionPoint,这个top1别管:

 然后我们添加上敌人,其实这里敌人没什么要讲的因为都是我们之前就做过的,直接预制体拖上去就完事了

 主要是要修复这个Zombie的bug,我们之前做的Walker.cs脚本有问题,现在让我们修复bug,问题在EndStopping中:我就说之前怎么会莫名其妙的转身,

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

public class Walker : MonoBehaviour
{
    [Header("Structure")]
    //检测玩家的脚本一个不能少
    [SerializeField] private LineOfSightDetector lineOfSightDetector;
    [SerializeField] private AlertRange alertRange; 

    //每一个敌人的四件公式化挂载rb2d,col2d,animator,audiosource,再加一个摄像头和hero位置
    private Rigidbody2D body;
    private Collider2D bodyCollider;
    private tk2dSpriteAnimator animator;
    private AudioSource audioSource;
    private Camera mainCamera;
    private HeroController hero;

    private const float CameraDistanceForActivation = 60f;
    private const float WaitHeroXThreshold = 1f; //距离玩家X方向上的极限距离值

    [Header("Configuration")]
    [SerializeField] private bool ambush; //是否埋伏
    [SerializeField] private string idleClip; //idle的动画片段名字
    [SerializeField] private string turnClip; //turn的动画片段名字
    [SerializeField] private string walkClip; //walk的动画片段名字
    [SerializeField] private float edgeXAdjuster; //检测墙沿x上的增加值
    [SerializeField] private bool preventScaleChange; //是否防止x轴的localscale发生变化
    [SerializeField] private bool preventTurn; //是否阻止转向
    [SerializeField] private float pauseTimeMin; //停止不动的时间
    [SerializeField] private float pauseTimeMax;
    [SerializeField] private float pauseWaitMin; //走路的时间
    [SerializeField] private float pauseWaitMax;
    [SerializeField] private bool pauses;  //是否需要静止状态
    [SerializeField] private float rightScale; //开始时的x轴方向
    [SerializeField] public bool startInactive; //开始时不活跃
    [SerializeField] private int turnAfterIdlePercentage; //Idle状态过后进入转身Turn状态的概率

    [SerializeField] private float turnPause; //设置转身的冷却时间
    [SerializeField] private bool waitForHeroX; //是否等待玩家X方向到位
    [SerializeField] private float waitHeroX; //等待玩家X方向距离
    [SerializeField] public float walkSpeedL; //向左走路的速度
    [SerializeField] public float walkSpeedR;//向右走路的速度
    [SerializeField] public bool ignoreHoles; //是否忽略洞
    [SerializeField] private bool preventTurningToFaceHero; //防止转向玩家的位置

    [SerializeField] private Walker.States state;
    [SerializeField] private Walker.StopReasons stopReason;
    private bool didFulfilCameraDistanceCondition; //暂时没有用到
    private bool didFulfilHeroXCondition; //暂时没有用到
    private int currentFacing;//Debug的时候可以在前面加个[SerializeField]
    private int turningFacing;
    //三个计时器且顾名思义
    private float walkTimeRemaining;
    private float pauseTimeRemaining;
    private float turnCooldownRemaining;

    protected void Awake()
    {
	body = GetComponent<Rigidbody2D>();
	bodyCollider = GetComponent<BoxCollider2D>();
	animator = GetComponent<tk2dSpriteAnimator>();
	audioSource = GetComponent<AudioSource>();
    }

    protected void Start()
    {
	mainCamera = Camera.main;
	hero = HeroController.instance;
	if(currentFacing == 0)
	{
	    currentFacing = ((transform.localScale.x * rightScale >= 0f) ? 1 : -1); //左边是-1,右边是1
	}
	if(state == States.NotReady)
	{
	    turnCooldownRemaining = -Mathf.Epsilon;
	    BeginWaitingForConditions();
	}
    }

    /// <summary>
    /// 我们创建另一个状态机,分为四种状态,每一种都有Update和Stop的方法。
    /// </summary>
    protected void Update()
    {
	turnCooldownRemaining -= Time.deltaTime;
	switch (state)
	{
	    case States.WaitingForConditions:
		UpdateWaitingForConditions();
		break;
	    case States.Stopped:
		UpdateStopping();
		break;
	    case States.Walking:
		UpdateWalking();
		break;
	    case States.Turning:
		UpdateTurning();
		break;
	    default:
		break;
	}
    }

    /// <summary>
    /// 从Waiting状态进入开始移动状态(不一定是Walk也可能是Turn)
    /// </summary>
    public void StartMoving()
    {
	if(state == States.Stopped || state == States.WaitingForConditions)
	{
	    startInactive = false;
	    int facing;
	    if(currentFacing == 0)
	    {
		facing = UnityEngine.Random.Range(0, 2) == 0 ? -1 : 1;
	    }
	    else
	    {
		facing = currentFacing;
	    }
	    BeginWalkingOrTurning(facing);
	}
	Update();
    }

    /// <summary>
    /// 在需要时取消转向
    /// </summary>
    public void CancelTurn()
    {
	if(state == States.Turning)
	{
	    BeginWalking(currentFacing);
	}
    }

    public void Go(int facing)
    {
	turnCooldownRemaining = -Time.deltaTime;
	if(state == States.Stopped || state == States.Walking)
	{
	    BeginWalkingOrTurning(facing);
	}
	else if(state == States.Turning && currentFacing == facing)
	{
	    CancelTurn();
	}
	Update();
    }

    public void ReceiveGoMessage(int facing) //TODO:
    {
	if(state != States.Stopped || stopReason != StopReasons.Controlled)
	{
	    Go(facing);
	}
    }

    /// <summary>
    /// 被脚本StopWalker.cs调用,更改reason为controlled
    /// </summary>
    /// <param name="reason"></param>
    public void Stop(StopReasons reason)
    {
	BeginStopped(reason);
    }

    /// <summary>
    /// 更改turningFacing和currentFacing,属于Turn状态的行为
    /// </summary>
    /// <param name="facing"></param>
    public void ChangeFacing(int facing)
    {
	if(state == States.Turning)
	{
	    turningFacing = facing;
	    currentFacing = -facing;
	    return;
	}
	currentFacing = facing;
    }

    /// <summary>
    /// 开始进入等待状态
    /// </summary>
    private void BeginWaitingForConditions()
    {
	state = States.WaitingForConditions;
	didFulfilCameraDistanceCondition = false;
	didFulfilHeroXCondition = false;
	UpdateWaitingForConditions();
    }

    /// <summary>
    /// 在Update以及BeginWaitingForConditions两大函数中调用,更新等待状态下的行为
    /// </summary>
    private void UpdateWaitingForConditions()
    {
	if (!didFulfilCameraDistanceCondition && (mainCamera.transform.position - transform.position).sqrMagnitude < CameraDistanceForActivation * CameraDistanceForActivation)
	{
	    didFulfilCameraDistanceCondition = true;
	}
	if(didFulfilCameraDistanceCondition && !didFulfilHeroXCondition && hero != null && 
	    Mathf.Abs(hero.transform.position.x - waitHeroX) < WaitHeroXThreshold) //TODO:
	{
	    didFulfilHeroXCondition = true;
	}
	if(didFulfilCameraDistanceCondition && (!waitForHeroX || didFulfilHeroXCondition) && !startInactive && !ambush)
	{
	    BeginStopped(StopReasons.Bored);
	    StartMoving();
	}
    }

    /// <summary>
    /// 开始进入停止状态
    /// </summary>
    /// <param name="reason"></param>
    private void BeginStopped(StopReasons reason)
    {
	state = States.Stopped;
	stopReason = reason;
	if (audioSource)
	{
	    audioSource.Stop();
	}
	if(reason == StopReasons.Bored)
	{
	    tk2dSpriteAnimationClip clipByName = animator.GetClipByName(idleClip);
	    if(clipByName != null)
	    {
		animator.Play(clipByName);
	    }
	    body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f)); //相当于把x方向上的速度设置为0
	    if (pauses)
	    {
		pauseTimeRemaining = UnityEngine.Random.Range(pauseTimeMin, pauseTimeMax);
		return;
	    }
	    EndStopping();
	}
    }

    /// <summary>
    /// 在Update中被调用,执行停止Stop状态的行为
    /// </summary>
    private void UpdateStopping()
    {
	if(stopReason == StopReasons.Bored)
	{
	    pauseTimeRemaining -= Time.deltaTime;
	    if(pauseTimeRemaining <= 0f)
	    {
		EndStopping();
	    }
	}
    }

    /// <summary>
    /// 终止停止状态
    /// </summary>
    private void EndStopping()
    {
	if(currentFacing == 0)
	{
	    BeginWalkingOrTurning(UnityEngine.Random.Range(0, 2) == 0 ? 1 : -1);
	    return;
	}
	if(UnityEngine.Random.Range(0,100) < turnAfterIdlePercentage)
	{
	    BeginTurning(-currentFacing);
	    return;
	}
	BeginWalking(currentFacing); //这里应该是开始行走Walk而不是开始转向Turn
    }

    /// <summary>
    /// 要不走路要不转身
    /// </summary>
    /// <param name="facing"></param>
    private void BeginWalkingOrTurning(int facing)
    {
	if(currentFacing == facing)
	{
	    BeginWalking(facing);
	    return;
	}
	BeginTurning(facing);
    }

    /// <summary>
    /// 开始进入Walking状态
    /// </summary>
    /// <param name="facing"></param>
    private void BeginWalking(int facing)
    {
	state = States.Walking;
	animator.Play(walkClip);
	if (!preventScaleChange)
	{
	    transform.SetScaleX(facing * rightScale);
	}
	walkTimeRemaining = UnityEngine.Random.Range(pauseWaitMin, pauseWaitMax);
	if (audioSource)
	{
	    audioSource.Play();
	}
	body.velocity = new Vector2((facing > 0) ? walkSpeedR : walkSpeedL,body.velocity.y);
    }

    /// <summary>
    /// 在Update中被调用,动态执行Walking状态,根据情况决定是否要进入Turning状态或者Stopped状态
    /// </summary>
    private void UpdateWalking()
    {
	if(turnCooldownRemaining <= 0f)
	{
	    Sweep sweep = new Sweep(bodyCollider, 1 - currentFacing, Sweep.DefaultRayCount,Sweep.DefaultSkinThickness);
	    if (sweep.Check(transform.position, bodyCollider.bounds.extents.x + 0.5f, LayerMask.GetMask("Terrain")))
	    {
		BeginTurning(-currentFacing);
		return;
	    }
	    if (!preventTurningToFaceHero && (hero != null && hero.transform.GetPositionX() > transform.GetPositionX() != currentFacing > 0) && lineOfSightDetector != null && lineOfSightDetector.CanSeeHero && alertRange != null && alertRange.IsHeroInRange)
	    {
		BeginTurning(-currentFacing);
		return;
	    }
	    if (!ignoreHoles)
	    {
		Sweep sweep2 = new Sweep(bodyCollider, DirectionUtils.Down, Sweep.DefaultRayCount, 0.1f);
		if (!sweep2.Check((Vector2)transform.position + new Vector2((bodyCollider.bounds.extents.x + 0.5f + edgeXAdjuster) * currentFacing, 0f), 0.25f, LayerMask.GetMask("Terrain")))
		{
		    BeginTurning(-currentFacing);
		    return;
		}
	    }
	}
	if (pauses)
	{
	    walkTimeRemaining -= Time.deltaTime;
	    if(walkTimeRemaining <= 0f)
	    {
		BeginStopped(StopReasons.Bored);
		return;
	    }
	}
	body.velocity = new Vector2((currentFacing > 0) ? walkSpeedR : walkSpeedL, body.velocity.y);
    }

    private void BeginTurning(int facing)
    {
	state = States.Turning;
	turningFacing = facing;
	if (preventTurn)
	{
	    EndTurning();
	    return;
	}
	turnCooldownRemaining = turnPause;
	body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f));
	animator.Play(turnClip);
	FSMUtility.SendEventToGameObject(gameObject, (facing > 0) ? "TURN RIGHT" : "TURN LEFT", false);
    }
    
   /// <summary>
   /// 在Update中被调用,执行Turning转身状态。
   /// </summary>
    private void UpdateTurning()
    {
	body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f));
	if (!animator.Playing)
	{
	    EndTurning();
	}
    }

    /// <summary>
    /// 被UpdateTurning()调用,当动画播放完成后切换到Walking状态。
    /// 被BeginTurning()调用,当preventTurn为true时就不再向下执行了。
    /// </summary>
    private void EndTurning()
    {
	currentFacing = turningFacing;
	BeginWalking(currentFacing);
    }

    /// <summary>
    /// 就清空turnCooldownRemaining
    /// </summary>
    public void ClearTurnCoolDown()
    {
	turnCooldownRemaining = -Mathf.Epsilon;
    }

    public enum States
    {
	NotReady,
	WaitingForConditions,
	Stopped,
	Walking,
	Turning
    }

    public enum StopReasons
    {
	Bored,
	Controlled
    }

}

然后我们在面板中重新设置好参数:

 

2.制作更多可交互对象

这个场景里可交互的貌似只有这个杆,我们添加好它的顶部和底部并设置好位置,添加上layer然后新建脚本:BreakablePole.cs

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

public class BreakablePole : MonoBehaviour,IHitResponder
{
    [SerializeField] private SpriteRenderer spriteRenderer;
    [SerializeField] private Sprite brokenSprite;
    [SerializeField] private float inertBackgroundThreshold;
    [SerializeField] private float inertForegroundThreshold;
    [SerializeField] private AudioSource audioSourcePrefab;
    [SerializeField] private RandomAudioClipTable hitClip;
    [SerializeField] private GameObject slashImpactPrefab;
    [SerializeField] private Rigidbody2D top;

    protected void Reset()
    {
	inertBackgroundThreshold = -1f;
	inertForegroundThreshold = -1f;
    }

    protected void Start()
    {
	float z = transform.position.z;
	if(z < inertBackgroundThreshold || z > inertForegroundThreshold)
	{
	    enabled = false;
	    return;
	}
    }

    public void Hit(HitInstance damageInstance)
    {
	int cardinalDirection = DirectionUtils.GetCardinalDirection(damageInstance.Direction);
	if (cardinalDirection != 2 && cardinalDirection != 0)
	{
	    return;
	}
	spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b,0f);
	Transform transform = Instantiate(slashImpactPrefab).transform;
	transform.eulerAngles = new Vector3(0f, 0f, Random.Range(340f, 380f));
	Vector3 localScale = transform.localScale;
	localScale.x = ((cardinalDirection == 2) ? -1f : 1f);
	localScale.y = 1f;
	hitClip.SpawnAndPlayOneShot(audioSourcePrefab, base.transform.position);
	if (top != null)
	{
	    top.gameObject.SetActive(true);
	    float num = (cardinalDirection == 2) ? Random.Range(120, 140) : Random.Range(40, 60);
	    top.transform.localScale = new Vector3(localScale.x, localScale.y, top.transform.localScale.z);
	    top.velocity = new Vector2(Mathf.Cos(num * 0.017453292f), Mathf.Sin(num * 0.017453292f)) * 5f;
	    top.transform.Rotate(new Vector3(0f, 0f, num));
	    base.enabled = false;
	}
    }
}

 然后设置好参数:

二、第二个代表性场景

1.制作更多敌人

然后就是大家喜闻乐见的长场景Crossroads_07,这个场景相当于一个区域的中转站,既可以向左走去虫爷爷和苍绿之境,或者打boss躁郁的毛里克,亦可以向右走去打假骑士和苍蝇之母,鹿角站,矿井等等,不过这些都是后话了,我们先来把地图做好,添加上对应的TranstionPoint:

其实做到这里我才意识到要把这四个 TranstionPoint做成预制体。

然后敌人自然是到处飞的苍蝇fly了:

2.制作可交互对象

为了让场景看起来生动,我们可以添加背景板里飞走的蚊子buzzer,用particlesystem来实现

第二个:

 

然后就是踩一下会发出声音的平台:

 我们来给它们新建一个脚本LiftPlatform.cs:实现了个功能当角色踩上去时上下移动一下,播放粒子系统和声音:

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

public class LiftPlatform : MonoBehaviour
{
    public GameObject part1;
    public GameObject part2;
    public ParticleSystem dustParticle;
    public AudioSource source;
    private float part1_start_y;
    private float part2_start_y;
    private int state;
    private float timer;

    private void Start()
    {
	part1_start_y = part1.transform.position.y;
	part2_start_y = part2.transform.position.y;
    }

    private void Update()
    {
	if(state == 1)
	{
	    if (timer < 0.125f)
	    {
		part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - timer * 0.75f, part1.transform.position.z);
		part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - timer * 0.75f, part2.transform.position.z);
		timer += Time.deltaTime;
	    }
	    else
	    {
		part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - 0.09f, part1.transform.position.z);
		part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - 0.09f, part2.transform.position.z);
		state = 2;
		timer = 0.12f;
	    }
	}
	if(state == 2)
	{
	    if (timer > 0f)
	    {
		part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - timer * 0.75f, part1.transform.position.z);
		part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - timer * 0.75f, part2.transform.position.z);
		timer -= Time.deltaTime;
		return;
	    }
	    part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y, part1.transform.position.z);
	    part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y, part2.transform.position.z);
	    state = 0;
	}
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
	if(state == 0 && collision.collider.gameObject.layer != LayerMask.NameToLayer("Item") && collision.gameObject.layer != LayerMask.NameToLayer("Particle") &&  collision.gameObject.layer != LayerMask.NameToLayer("Enemies") && collision.GetSafeContact().Normal.y < 0.1f)
	{
	    source.pitch = Random.Range(0.85f, 1.15f);
	    source.Play();
	    dustParticle.Play();
	    state = 0;
	    timer = 0f;
	}
    }

}

 

三、第三个代表性场景

1.制作更多的敌人

其实这个场景我就想将新制作的敌人的:没错就是这个拿骨钉盾牌的僵尸虫

我们回到tk2dSprite和tk2danimator给它创建好:

 

 

你只需要记住,A是anticipate攻击准备阶段的动画,L是Lunge突刺动画,S是攻击时的动画,CD是冷却时候的动画, 然后剩下的dddd。这个Bump就是反弹骨钉攻击的动画。OK说的差不多了,然后Unsheild和sheild动画就是反过来的。

然后就是添加相应的脚本到场景中:

s上面的脚本我在之前的文章都讲过了,除了这个EnemyDeathEffects:

using System;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;

public class EnemyDeathEffects : MonoBehaviour
{
    [SerializeField] private GameObject corpsePrefab;
    [SerializeField] private bool corpseFacesRight;
    [SerializeField] private float corpseFlingSpeed;
    [SerializeField] public Vector3 corpseSpawnPoint;
    [SerializeField] private string deathBroadcastEvent;
    [SerializeField] private Vector3 effectOrigin;
    [SerializeField] private bool lowCorpseArc;

    [SerializeField] private EnemyDeathTypes enemyDeathType;
    [SerializeField] protected AudioSource audioPlayerPrefab;
    [SerializeField] protected AudioEvent enemyDeathSwordAudio;
    [SerializeField] protected AudioEvent enemyDamageAudio;
    [SerializeField] protected AudioClip enemyDeathSwordClip;
    [SerializeField] protected AudioClip enemyDamageClip;
    [SerializeField] private AudioMixerSnapshot audioSnapshotOnDeath;

    [SerializeField] protected GameObject deathWaveInfectedPrefab;
    [SerializeField] protected GameObject deathWaveInfectedSmallPrefab;

    [SerializeField] private bool recycle;
    [SerializeField] private bool rotateCorpse; //尸体需要旋转吗

    [SerializeField] protected GameObject dustPuffMedPrefab;
    [SerializeField] protected GameObject deathPuffLargePrefab;

    protected GameObject corpse;
    private bool didFire;
    [HideInInspector]
    public bool doKillFreeze = true;
    protected void Start()
    {
	PreInstantiate();
    }

    public void PreInstantiate()
    {
	if(!corpse && corpsePrefab)
	{
	    corpse = Instantiate(corpsePrefab, transform.position + corpseSpawnPoint, Quaternion.identity, transform);
	    tk2dSprite[] componentInChildrens = corpse.GetComponentsInChildren<tk2dSprite>(true);
	    for (int i = 0; i < componentInChildrens.Length; i++)
	    {
		componentInChildrens[i].ForceBuild();
	    }
	    corpse.SetActive(false);
	}
    }

    public void RecieveDeathEvent(float? attackDirection, bool resetDeathEvent = false, bool spellBurn = false, bool isWatery = false)
    {
	if (didFire)
	    return;
	didFire = true;
	
	if(corpse != null)
	{
	    EmitCorpse(attackDirection, isWatery, spellBurn);
	}
	if (!isWatery)
	{
	    EmitEffects();
	}
	if (doKillFreeze)
	{
	    GameManager.instance.FreezeMoment(1);
	}
	if (enemyDeathType == EnemyDeathTypes.Infected || enemyDeathType == EnemyDeathTypes.LargeInfected || enemyDeathType == EnemyDeathTypes.SmallInfected || enemyDeathType == EnemyDeathTypes.Uninfected )
	{
	    EmitEssence();
	}
	if (audioSnapshotOnDeath != null)
	{
	    audioSnapshotOnDeath.TransitionTo(2f);
	}
	if (!string.IsNullOrEmpty(deathBroadcastEvent))
	{
	    Debug.LogWarningFormat(this, "Death broadcast event '{0}' not implemented!", new object[]
	    {
		deathBroadcastEvent
	    });
	}
	if (resetDeathEvent)
	{
	    FSMUtility.SendEventToGameObject(gameObject, "CENTIPEDE DEATH", false);
	    didFire = false;
	    return;
	}
	if (recycle)
	{
	    PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(gameObject, "health_manager_enemy");
	    if(playMakerFSM != null)
	    {
		playMakerFSM.FsmVariables.GetFsmBool("Activated").Value = false;
	    }
	    HealthManager component2 = GetComponent<HealthManager>();
	    if(component2 != null)
	    {
		component2.SetIsDead(false);
	    }
	    didFire = false;
	    //TODO:
	    return;
	}
	Destroy(gameObject);
    }

    private void EmitCorpse(float? attackDirection, bool isWatery, bool spellBurn)
    {
	if (corpse == null)
	    return;
	corpse.transform.SetParent(null);
	corpse.transform.SetPositionZ(UnityEngine.Random.Range(-0.08f, -0.09f));
	corpse.SetActive(true);
	PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(corpse, "corpse");
	if(playMakerFSM != null)
	{
	    FsmBool fsmBool = playMakerFSM.FsmVariables.GetFsmBool("spellBurn");
	    if(fsmBool!= null)
	    {
		fsmBool.Value = false;
	    }
	}
	Corpse component = corpse.GetComponent<Corpse>();
	if (component)
	{
	    component.Setup(isWatery, spellBurn);
	}
	if (isWatery)
	{
	    return;
	}
	corpse.transform.SetRotation2D(rotateCorpse ? transform.GetRotation2D():0f);
	if(Mathf.Abs(transform.eulerAngles.z) >= 45f)
	{
	    Collider2D component2 = GetComponent<Collider2D>();
	    Collider2D component3 = corpse.GetComponent<Collider2D>();
	    if(!rotateCorpse && component2 && component3)
	    {
		Vector3 b = component2.bounds.center - component3.bounds.center;
		b.z = 0f;
		corpse.transform.position += b;
	    }
	}
	float d = 1f;
	if(attackDirection == null)
	{
	    d = 0f;
	}
	int cardinalDirection = DirectionUtils.GetCardinalDirection(attackDirection.GetValueOrDefault());
	Rigidbody2D component4 = corpse.GetComponent<Rigidbody2D>();
	if(component4 != null && !component4.isKinematic)
	{
	    float num = corpseFlingSpeed;
	    float num2;
	    switch (cardinalDirection)
	    {
		case 0:
		    num2 = lowCorpseArc ? 10f : 60f;
		    corpse.transform.SetScaleX(corpse.transform.localScale.x * (corpseFacesRight ? -1f : 1f) * Mathf.Sign(transform.localScale.x));
		    break;
		case 1:
		    num2 = UnityEngine.Random.Range(75f, 105f);
		    num *= 1.3f;
		    break;
		case 2:
		    num2 = lowCorpseArc ? 170f : 120f;
		    corpse.transform.SetScaleX(corpse.transform.localScale.x * (corpseFacesRight ? 1f : -1f) * Mathf.Sign(transform.localScale.x));
		    break;
		case 3:
		    num2 = 270f;
		    break;
		default:
		    num2 = 90f;
		    break;
	    }
	    component4.velocity = new Vector2(Mathf.Cos(num2 * 0.017453292f), Mathf.Sin(num2 * 0.017453292f)) * num * d;
	}
    }

    private void EmitEffects()
    {
	EnemyDeathTypes enemyDeathTypes = enemyDeathType;
	if(enemyDeathTypes == EnemyDeathTypes.Infected)
	{
	    EmitInfectedEffects();
	    return;
	}
	if (enemyDeathTypes == EnemyDeathTypes.SmallInfected)
	{
	    EmitSmallInfectedEffects();
	    return;
	}
	if (enemyDeathTypes != EnemyDeathTypes.LargeInfected)
	{
	    Debug.LogWarningFormat(this, "Enemy death type {0} not implemented!", new object[]
	    {
		enemyDeathType
	    });
	    return;
	}
	EmitLargeInfectedEffects();
    }

    private void EmitLargeInfectedEffects()
    {
	AudioEvent audioEvent = default(AudioEvent);
	audioEvent.Clip = enemyDeathSwordClip;
	audioEvent.PitchMin = 0.75f;
	audioEvent.PitchMax = 0.75f;
	audioEvent.Volume = 1f;
	audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
	audioEvent = default(AudioEvent);
	audioEvent.Clip = enemyDamageClip;
	audioEvent.PitchMin = 0.75f;
	audioEvent.PitchMax = 0.75f;
	audioEvent.Volume = 1f;
	audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
	if(corpse != null)
	{
	    SpriteFlash component = corpse.GetComponent<SpriteFlash>();
	    if(component != null)
	    {
		component.flashInfected();
	    }
	}
	if (!(deathPuffLargePrefab == null))
	{
	    Instantiate(deathPuffLargePrefab, transform.position + effectOrigin, Quaternion.identity);
	}
	ShakeCameraIfVisible("AverageShake");
	if (!(deathWaveInfectedPrefab == null))
	{
	    GameObject gameObject = Instantiate(deathWaveInfectedPrefab, transform.position + effectOrigin, Quaternion.identity);
	    gameObject.transform.SetScaleX(2f);
	    gameObject.transform.SetScaleY(2f);
	}
	GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 75, 80, 20f, 25f, 0f, 360f, null);
    }

    private void EmitSmallInfectedEffects()
    {
	AudioEvent audioEvent = default(AudioEvent);
	audioEvent.Clip = enemyDeathSwordClip;
	audioEvent.PitchMin = 1.2f;
	audioEvent.PitchMax = 1.4f;
	audioEvent.Volume = 1f;
	audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
	audioEvent = default(AudioEvent);
	audioEvent.Clip = enemyDamageClip;
	audioEvent.PitchMin = 1.2f;
	audioEvent.PitchMax = 1.4f;
	audioEvent.Volume = 1f;
	audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
	if (deathWaveInfectedSmallPrefab != null)
	{
	    GameObject gameObject = Instantiate(deathWaveInfectedSmallPrefab, transform.position + effectOrigin,Quaternion.identity);
	    Vector3 localScale = gameObject.transform.localScale;
	    localScale.x = 0.5f;
	    localScale.y = 0.5f;
	    gameObject.transform.localScale = localScale;
	}
	GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 8, 10, 15f, 20f, 0, 360, null);
    }

    private void EmitInfectedEffects()
    {
	EmitSound();
	if(corpse != null)
	{
	    SpriteFlash component = corpse.GetComponent<SpriteFlash>();
	    if(component != null)
	    {
		component.flashInfected();
	    }
	}
	GameObject gameObject = Instantiate(deathWaveInfectedPrefab, transform.position + effectOrigin, Quaternion.identity);
	gameObject.transform.SetScaleX(1.25f);
	gameObject.transform.SetPositionY(1.25f);
	GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 8, 10, 15f, 20f, 0, 360, null);
	Instantiate(dustPuffMedPrefab, transform.position + effectOrigin, Quaternion.identity);
	ShakeCameraIfVisible("EnemyKillShake");
    }

    private void EmitSound()
    {
	enemyDeathSwordAudio.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
	enemyDamageAudio.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);
    }

    private void EmitEssence()
    {
	//TODO:和梦之钉有关的
	PlayerData playerData = GameManager.instance.playerData;
	if (!playerData.hasDreamNail)
	{
	    return; 
	}
    }

    protected void ShakeCameraIfVisible(string eventName)
    {
	Renderer renderer = GetComponent<Renderer>();
	if (renderer == null)
	{
	    renderer = GetComponentInChildren<Renderer>();
	}
	if (renderer != null && renderer.isVisible)
	{
	    GameCameras.instance.cameraShakeFSM.SendEvent(eventName);
	}
    }

}

 在面板中添加好参数后,我们就到HealthManager中:

public class HealthManager : MonoBehaviour, IHitResponder
{
    private EnemyDeathEffects enemyDeathEffects;

     protected void Awake()
    {

	enemyDeathEffects = GetComponent<EnemyDeathEffects>();

    }

 public void Die(float? attackDirection, AttackTypes attackType, bool ignoreEvasion)
    {
	if (isDead)
	{ 
	    return;
	}
	if (sprite)
	{
	    sprite.color = Color.white;
	}
	FSMUtility.SendEventToGameObject(gameObject, "ZERO HP", false);
	if (hasSpecialDeath)
	{
	    NonFatalHit(ignoreEvasion);
	    return;
	}
	isDead = true;
	if(damageHero != null)
	{
	    damageHero.damageDealt = 0;
	}
	if(battleScene != null && !notifiedBattleScene)
	{
	    PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(battleScene, "Battle Control");
	    if(playMakerFSM != null)
	    {
		FsmInt fsmInt = playMakerFSM.FsmVariables.GetFsmInt("Battle Enemies");
		if(fsmInt != null)
		{
		    fsmInt.Value--;
		    notifiedBattleScene = true;
		}
	    }
	}
	if (enemyDeathEffects != null)
	{
	    if (attackType == AttackTypes.Generic)
	    {
		enemyDeathEffects.doKillFreeze = false;
	    }
	    enemyDeathEffects.RecieveDeathEvent(attackDirection, deathReset, attackType == AttackTypes.Spell, false);
	}
	SendDeathEvent();
	Destroy(gameObject); //TODO:
    }
}

还有我们将蚊子那期介绍的老朋友攻击距离检测,警戒距离检测        

 还有类似于玩家的slash的polygon collider2d,别忘了给它们添加上damagehero脚本

 还有一个就是当僵尸虫进入冲刺状态上播放的粒子系统dust:

接下来就到了我们老朋友playmakerFSM登场了,老规矩我先贴出来变量和事件然后逐个讲状态:

第一个状态当然是初始化了:

 每一帧都检测玩家是否到可视范围和攻击范围了

判断玩家位置: 

 

 

我们先做好playmakerFSM自定义脚本:

using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class SetInvincible : FsmStateAction
{
    [UIHint(UIHint.Variable)]
    public FsmOwnerDefault target;
    public FsmBool Invincible;
    public FsmInt InvincibleFromDirection;

    public override void Reset()
    {
	target = new FsmOwnerDefault();
	Invincible = null;
	InvincibleFromDirection = null;
    }

    public override void OnEnter()
    {
	GameObject safe = target.GetSafe(this);
	if (safe != null)
	{
	    HealthManager component = safe.GetComponent<HealthManager>();
	    if (component != null)
	    {
		if (!Invincible.IsNone)
		{
		    component.IsInvincible = Invincible.Value;
		}
		if (!InvincibleFromDirection.IsNone)
		{
		    component.InvincibleFromDirection = InvincibleFromDirection.Value;
		}
	    }
	}
	Finish();
    }

}
using HutongGames.PlayMaker;
using UnityEngine;


[ActionCategory("Hollow Knight")]
public class SetWalkerFacing : WalkerAction
{
    public FsmBool walkRight;
    public FsmBool randomStartDir;

    public override void Reset()
    {
	base.Reset();
	walkRight = new FsmBool
	{
	    UseVariable = true
	};
	randomStartDir = new FsmBool();
    }
    /// <summary>
    /// 调用Walker.cs中的ChangeFacing函数来改变朝向
    /// </summary>
    /// <param name="walker"></param>
    protected override void Apply(Walker walker)
    {
	if (randomStartDir.Value)
	{
	    walker.ChangeFacing((Random.Range(0, 2) == 0) ? -1 : 1);
	    return;
	}
	if (!walkRight.IsNone)
	{
	    walker.ChangeFacing(walkRight.Value ? 1 : -1);
	}
    }

}

 

 

 剩下三个都差不多,你只需要设置好tk2d动画,Lung1 Speed和Lung2 Speed的正负,判断不同方向的事件,以及sheild格挡的方向:

 

触发BLOCKED HIT的事件该执行的状态:

这个BLOCKED HIT的事件在HealthManager.cs中会触发的,让我们回到HealthManager.cs脚本中:

 public void Invincible(HitInstance hitInstance)
    {
	int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	directionOfLastAttack = cardinalDirection;
	FSMUtility.SendEventToGameObject(gameObject, "BLOCKED HIT", false);
	FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);
	if (!(GetComponent<DontClinkGates>() != null))
	{
	    FSMUtility.SendEventToGameObject(gameObject, "HIT", false);

	    if(hitInstance.AttackType == AttackTypes.Nail)
	    {
		if(cardinalDirection == 0)
		{
		    HeroController.instance.RecoilLeft();
		}
		else if(cardinalDirection == 2)
		{
		    HeroController.instance.RecoilRight();
		}
	    }

	    Vector2 v;
	    Vector3 eulerAngles;
	    if (boxCollider != null)
	    {
		switch (cardinalDirection)
		{
		    case 0:
			v = new Vector2(transform.GetPositionX() + boxCollider.offset.x - boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
			eulerAngles = new Vector3(0f, 0f, 0f);
			break;
		    case 1:
			v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Max(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y - boxCollider.size.y * 0.5f));
			eulerAngles = new Vector3(0f, 0f, 90f);
			break;
		    case 2:
			v = new Vector2(transform.GetPositionX() + boxCollider.offset.x + boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());
			eulerAngles = new Vector3(0f, 0f, 180f);
			break;
		    case 3:
			v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Min(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y + boxCollider.size.y * 0.5f));
			eulerAngles = new Vector3(0f, 0f, 270f);
			break;
		    default:
			break;
		}
	    }
	    else
	    {
		v = transform.position;
		eulerAngles = new Vector3(0f, 0f, 0f);
	    }
	}
	evasionByHitRemaining = 0.15f;
    }

那么这个方法在哪里被调用呢,当然是我们的HIT方法了,如果你还有印象,这个就是继承接口IHitResponder要实现的方法:

public void Hit(HitInstance hitInstance)
    {
	if (isDead)
	{
	    return;
	}
	if(evasionByHitRemaining > 0f) 
	{ 
	    return;
	}
	if(hitInstance.DamageDealt < 0f)
	{
	    return;
	}
	FSMUtility.SendEventToGameObject(hitInstance.Source, "DEALT DAMAGE", false);
	int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));
	if (IsBlockingByDirection(cardinalDirection, hitInstance.AttackType))
	{
	    Invincible(hitInstance);
	    return;
	}
	TakeDamage(hitInstance);
    }

 判断当前攻击方向是否格挡:

[Header("Invincible")]
    [SerializeField] private bool invincible;
    [SerializeField] private int invincibleFromDirection;

public bool IsBlockingByDirection(int cardinalDirection,AttackTypes attackType)
    {
//法术攻击无法格挡
	if(attackType == AttackTypes.Spell && gameObject.CompareTag("Spell Vulnerable"))
	{
	    return false;
	}
//不是无敌无法格挡
	if (!invincible)
	{
	    return false;
	}
//没有确切的方向无法格挡
	if(invincibleFromDirection == 0)
	{
	    return true;
	}
	switch (cardinalDirection)
	{
	    case 0:
	    {
		int num = invincibleFromDirection;
		if (num <= 5)
		{
		    if (num != 1 && num != 5)
		    {
		     return false;
		    }
		 }
		else if (num != 8 && num != 10)
		{
		    return false;
		}
		return true;
	    }   
	    case 1:
	    {
		int num = invincibleFromDirection;
		return num == 2 || num - 5 <= 4;
	    }
	    case 2:
		{
		    int num = invincibleFromDirection;
		    if (num <= 6)
		    {
			if (num != 3 && num != 6)
			{
			    return false;
			}
		    }
		    else if (num != 9 && num != 11)
		    {
			return false;
		    }
		    return true;
		}
	    case 3:
		{
		    int num = invincibleFromDirection;
		    return num == 4 || num - 7 <= 4;
		}
	    default:
		return false;
	}

    }

格挡以后自然是要对玩家发起攻击了:

首先进入准备阶段:

向前冲刺阶段:

攻击阶段,在这里就要打开我们创建的slash的碰撞箱了,同时将僵尸虫的速度位置为0 

 冷却阶段,关闭碰撞箱:

然后突然虚晃一枪接着对玩家发动二段攻击,没有准备阶段直接进入冲刺攻击阶段

 

 二阶段的冷却:

三阶段的再次冲刺攻击阶段: 

 

三阶段的攻击阶段:

三阶段的冷却阶段,应该不叫冷却而是叫停止攻击阶段:

重置walker状态: 

除了自动攻击我们还有主动攻击阶段,内容我就不赘述了直接上图:

 

 还有就是玩家离开可视范围和攻击距离发送LEFT RANGE事件,把举起的盾牌放下来


总结

我们来看看上述讲到的三个场景的效果(上面的UI先别管我之后会完成的):

虫子的转身没问题的

 

平台没问题的 

 然后僵尸盾牌虫:直接攻击Attack 1

 它在反击后的攻击是水平方向的,因此我可以下劈它

由于我没做格挡时的动画,虽然不明显但是还是能看到敌人并没有因为我的攻击而受到伤害。。

三段攻击也有,但我忘了截屏了就先这样吧,下一期我们来制作更多的敌人和更多的场景 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/899008.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB安装配置及配置和启动服务

MongoDB 安装配置 附&#xff1a;MongoDB官网下载地址&#xff1a; https://www.mongodb.com/download-center/community 注&#xff1a; 官网可以下载最新版的MongoDB安装包&#xff0c;有MSI安装版和ZIP安装版。我们课堂上使用4.4.4的ZIP安装版。安装版参考博客&#xff1…

【redis】基础指令|数据结构总览|单线程架构分析

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; 前言&#xff1a;redis系类博客都是以redis5.0版本为基础&#xff01;&#xff01;&#xff01; 目录 Redis常见命令 基本全局命令 KEYS EXISTS DEL EXPIRE TTL TYPE 数据结构和内部编码 单线程架构 Redis…

群控系统服务端开发模式-数据库设计图

根据整理的业务需求可以发现&#xff0c;本系统数据库针对1.0版本就分两种库。第一类是管理层的数据库&#xff0c;分别是管理员表、角色表、菜单表、部门表、级别表。分别对应控制权限及数据权限。 一、管理层数据库设计图 二、业务层数据库设计图

潜水定位通信系统的功能和使用方法_鼎跃安全

潜水定位通信系统是保障潜水安全与作业高效的关键设备。它利用先进的声呐、无线电等技术&#xff0c;可精准定位潜水员位置。在水下能实现潜水员之间以及与水面的双向通信&#xff0c;确保信息及时传递。具备高可靠性和稳定性&#xff0c;即使在复杂水环境中也能正常运行。 一、…

智能体能和人工智能有什么区别?

智能体与人工智能&#xff08;AI&#xff09;之间存在明显的区别&#xff0c;尽管两者在技术和应用上有一定的重叠。 一、定义与范畴 人工智能&#xff08;AI&#xff09; 人工智能是指通过模拟、延伸和扩展人的智能&#xff0c;使计算机或其他智能设备具有人类智能的一种技术…

Redis --- 第六讲 --- 关于持久化

前言 持久化&#xff1a;MySQL的事务&#xff0c;有四大比较核心的特性 1、原子性 2、一致性 3、持久性 》 把数据存储到硬盘上 》持久&#xff0c;把数据存储在内存上》持久化。重启进程/重启主机之后&#xff0c;数据是否存在。 4、隔离性 Redis是一个内存数据库&#…

如何在忘记密码的情况下解锁 iPhone? 6 种方法分享

您是否因为没有密码而无法解锁您的 iPhone&#xff1f; 别担心&#xff0c;这种情况比你想象的更常见&#xff01;忘记密码是 iPhone 用户面临的最常见问题之一&#xff0c;而且可能非常令人沮丧 - 但不要绝望。 在这篇文章中&#xff0c;我们将与您分享绕过 iPhone 屏幕密码…

No provider available from registry RegistryDirectory

【中】No provider available from registry RegistryDirectory Dubbo 3.2.9Nacos 2.1.0 最近在做配置文件升级&#xff0c;服务比较多&#xff0c;之前的Dubbo配置各个服务写的比较乱&#xff0c;有的用Nacos上的 data-id&#xff0c;有的又是在自己的服务引入配置 遂准备统一…

记录一次从nacos配置信息泄露到redis写计划任务接管主机

经典c段打点开局。使用dddd做快速的打点发现某系统存在nacos权限绕过 有点怀疑是蜜罐&#xff0c;毕竟nacos这实在是有点经典 nacos利用 老规矩见面先上nacos利用工具打一波看看什么情况 弱口令nacos以及未授权访问&#xff0c;看这记录估计被光顾挺多次了啊 手动利用Nacos-…

软件测试与软件缺陷的基础知识

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

秋招面试题记录_半结构化面试

c八股(可能问的多一点) 1.简单说说C11语法特性 答&#xff1a; 1.auto以及decltype自动类型推导&#xff0c;避免手动声明复杂类型&#xff0c;减少冗长代码提升了可读性和安全性。 2.智能指针 自动释放内存 (具体说说) 有shared和unique 差异主要体现在所有权、内存开销、…

Tesseract OCR 安装

Tesseract OCR 的安装步骤因操作系统的不同而有所区别。以下是针对 Windows、macOS 和 Linux 系统的详细安装指导。 1. Windows 步骤&#xff1a; 下载 Tesseract 安装程序 访问 Tesseract GitHub Release 页面。下载最新版本的安装程序&#xff08;例如 .exe 文件&#xff0…

【小趴菜前端实习日记5】

实习日记5 一、vue3中如何使用router&#xff08;获取this)二、ts中用object定义类型太宽泛导致Ts无法推断出正确类型三、动态设置日记封面失败vite动态引入静态资源1.方法一vue3父子组件生命周期执行顺序 2.方法二3.方法三 四、打包问题总结1.The import.meta meta-property i…

整理—Redis

目录 Redis底层的数据结构 ZSet用过吗 Zset 底层是怎么实现的 跳表是怎么实现的&#xff1f; Redis为什么使用跳表而不是用B树? 压缩列表是怎么实现的&#xff1f; Redis 中的 listpack 哈希表是怎么扩容的&#xff1f; String 是使用什么存储的 Redis为什么快&#xf…

最好的ppt模板网站是哪个?做PPT不可错过的18个网站!

现在有很多PPT模板网站&#xff0c;但真正免费且高质量的不多&#xff0c;今天我就分享主流的国内外PPT模板下载网站&#xff0c;并且会详细分析这些网站的优缺点&#xff0c;这些网站都是基于个人实际使用经验的&#xff0c;免费站点会特别标注&#xff0c;让你可以放心下载&a…

C++:模板(2)

目录 非类型模板参数 模板的特化 概念 函数模板特化 类模板特化 全特化 偏特化 模板的分离编译 分离编译的概念 模板的分离编译 ​编辑 模板总结 非类型模板参数 模板参数分为类型形参与非类型形参。 类型形参&#xff1a;在模板参数列表中&#xff0c;跟在class…

STM32L1x 片上温度传感器采用ADC及工厂校准数据提升测量温度精度

背景 由于项目临时需要温度数据&#xff0c;又不想改动硬件了&#xff0c;反正对温度精度要求不算太高&#xff0c;索性就用MCU片上温度传感器的温度&#xff0c;来替代了。这里自己根据网上帖子做了一些测试用例尝试测温&#xff0c;但是&#xff0c;效果都不理想。发现ST官方…

得物App3D博物馆亮相“两博会”,正品保障助力消费体验升级

近日&#xff0c;2024中国体育文化博览会、中国体育旅游博览会&#xff08;以下简称“两博会”&#xff09;在苏州国际展览中心盛大开幕。本次展会汇聚了众多国内外体育文化、体育旅游领域的顶尖企业和品牌&#xff0c;共同展示体育产业的发展成果和最新趋势。在C展馆C21展位&a…

Adams函数构建器(Function Builder)教程来了

学会使用函数构建器是在进行Adams仿真分析的必备技能&#xff0c;通过函数构建器可以查询和使用Adams的各种设计时函数和运行时函数&#xff0c;并能够构建用户自己的函数&#xff0c;大多数情况下的力或者驱动都不是简单的数字&#xff0c;而是需要函数来驱动的&#xff0c;那…

GEE数据集:2001年-2019年全球土地覆被估算(GLanCE)

目录 简介 数据集说明 空间信息 代码1 代码2 代码链接 APP链接 结果 引用 许可 网址推荐 知识星球 机器学习 简介 全球土地覆被估算&#xff08;GLanCE&#xff09; 全球土地覆被估算&#xff08;GLanCE&#xff09;数据集利用 30 米空间分辨率的大地遥感卫星图…