Unity2D:从零开始制作一款跑酷游戏!
目录
成品展示
美术资源
制作步骤
场景预布设:
实现人物基础功能:
移动背景——横向卷轴:
生成障碍物:
生成敌人与攻击逻辑:
UI制作与重新开始:
导出游戏:
小结
大家小时候都玩过《天天酷跑》吧?这类游戏的原理很简单,非死即跑,主要用到的是2D卷轴技术。现在,我们一起来把经典复刻,从零开始做一款简单的跑酷类小游戏。
成品展示
回家的路
关于这款游戏的源文件与exe文件我已经同步上传到了github,想游玩的话复制下面的地址双击下载zip文件即可。
https://github.com/starmatch9/simple-parkour-game-Ascendant-Path-/releases/tag/v1.0.0
这里也提供了百度网盘的下载链接,点击最下方阅读原文即可下载:
https://pan.baidu.com/s/1AuaV1R0l9dwORnzzMSs1rg?pwd=y4mu
提取码: y4mu
美术资源
这里采用的都是Unity资源商店中的免费资源,可以根据喜好自行选择,在此列出我使用的资源。
背景:2D Adventure Backgrounds Pack
https://assetstore.unity.com/packages/2d/environments/2d-adventure-backgrounds-pack-309711
人物:Cute 2D - College Student
https://assetstore.unity.com/packages/2d/characters/cute-2d-college-student-198684
障碍物:Free Platform Game Assets
https://assetstore.unity.com/packages/2d/environments/free-platform-game-assets-85838
怪物:Fantasy Monster Pack: 5 Handcrafted 2D Creatures
https://assetstore.unity.com/packages/2d/characters/fantasy-monster-pack-5-handcrafted-2d-creatures-296211
GUI:Buttons Set
https://assetstore.unity.com/packages/2d/gui/buttons-set-211824
字体:Bubble Font (Free Version)
https://assetstore.unity.com/packages/2d/fonts/bubble-font-free-version-24987
制作步骤
场景预布设:
我们可以在包管理器:我的资产界面看到我们下载好的资源,现在将准备好的资源导入到编辑器中,能够看到名称为资源名称的文件夹出现在Asset目录下。
然后就可以先创建背景了:在场景中创建一个名为“Background”的空物体,用来管理我们的背景精灵。然后把我们的背景资源依次拖入到该物体的下方。因为我们的背景用不到Unity的物理系统,所以就可以不用更改精灵导入PPU,要调整大小的话,放入场景后改变Scale即可。
现在添加名为“Background”的排序图层,将所有的背景精灵放在这个排序图层上,用于以后确定这些背景图像渲染在最下方。
并调整图层顺序以获得正确的遮盖方式,对每个部分的位置进行移动以达到最佳状态。
当然不能忘记设置摄像机的正交大小,因为我选取的角色是带有骨骼动画的,覆盖了PPU等精灵导入设置,所以我们调整摄像机的正交大小以显示人物合适的大小。
按照同样的方法将角色导入,然后将预制件拖入场景中,调整摄像机正交大小,也别忘了缩放我们背景(父物体)的大小。
现在我们把平台放入场景,即人物脚踩的地方。因为需要在平台拼接时保持平滑,我采用了瓦片地图的方式。在场景中创建一个瓦片地图,会自动生成名为Grid的父物体,用来确定瓦片地图使用的网格坐标系。将瓦片地图改名为Platforms用来放置平台。
然后创建瓦片调色板(即平铺调色板),目标瓦片地图设置为我们刚刚创建的Platforms。将瓦片调色板的资源分文件存放便于区分。
然后用调色板绘制平台,注意要将平台地图的排序图层设置为Background,图层顺序放置中景与前景之间以达到预期效果。
到现在,我们的场景布局就完成了,现在开始考虑游戏各种功能的实现。
实现人物基础功能:
在这种跑酷类游戏中,是不需要真正控制角色移动的,只需要让背景、平台和障碍物不断朝左边运动即可。所以我们的人物只需要能够原地奔跑以及原地跳跃就足够了。
首先角色得能跑吧?创建一个动画控制器专门用来控制角色的动画。然后将场景中人物的控制器替换为我们自己创建的控制器,将资源中的奔跑动画拖入其中。
现在开始实现角色跳跃。详细的实现在之前的文章谈到过,所以这里不作过多说明。为我们的平台添加碰撞器,并添加一个复合碰撞器让碰撞器们平滑连接。
为角色添加胶囊碰撞器以及刚体组件,并调整其大小,用于参与物理模拟。同时为了防止其移动,我们冻结其水平方向的位置与旋转。
然后在动画动画器中添加跳跃动画,同时创建bool参数isJump判断是否要从奔跑切换为跳跃。在奔跑和跳跃之间添加过渡,一定不能勾选有退出时间,且过渡持续时间为0。
然后编写以下角色跳跃脚本。
Animator animator;
Rigidbody2D body;
public float jumpVelocity = 20;
//用来判断角色是否在平台上
bool onPlatform = true;
void Awake()
{animator = GetComponent<Animator>();body = GetComponent<Rigidbody2D>();
}
void Update()
{//按键J用来检测跳跃(暂定)if (onPlatform && Input.GetKeyDown(KeyCode.J)){body.velocity = new Vector2 (body.velocity.x, jumpVelocity);}
}
//这里同之前的文章一样,记得改平台的图层名称用来识别
private void OnCollisionEnter2D(Collision2D collision)
{if(collision.gameObject.layer == LayerMask.NameToLayer("Platform")){onPlatform = true;animator.SetBool("isJump", false);}
}
private void OnCollisionExit2D(Collision2D collision)
{if (collision.gameObject.layer == LayerMask.NameToLayer("Platform")){onPlatform = false;animator.SetBool("isJump", true);}
}
然后我们的角色跳跃功能就做好了。
因为这个角色资源里也包含了攻击动画(Attack)与滑板车动画(KickBoard),所以我们用相同的方式也加入这两个动画。
然后编写脚本:滑板车设置为按住空格键就一直滑行,松开就回到跑步状态。攻击就通过协程的方式让动作持续一段时间,后续可以加入攻击效果。
bool isAttack = false;
void Update()
{//按键J用来检测跳跃(暂定)if (onPlatform && Input.GetKeyDown(KeyCode.J)){body.velocity = new Vector2 (body.velocity.x, jumpVelocity);}//按住空格检测滑板车if (Input.GetKey(KeyCode.Space)){animator.SetBool("isKickBoard", true);}else{animator.SetBool("isKickBoard", false);}if (!isAttack && Input.GetKey(KeyCode.F)){isAttack = true;StartCoroutine(Attack());}
}
IEnumerator Attack()
{animator.SetBool("isAttack", true);yield return new WaitForSeconds(0.8f);animator.SetBool("isAttack", false);//等等一段时间后才能进行下一次攻击,相当于冷却时间yield return new WaitForSeconds(0.5f);isAttack = false;
}
那么现在角色的基本操作就已经实现完毕了。
移动背景——横向卷轴:
我们现在需要考虑的是角色的以上功能会用在什么场景下,跳跃用来避开障碍物,攻击用来打倒怪物,可以是释放出一个强大的冲击波,滑板车就暂且当一个装饰吧。
但想到这些物体都是随着背景朝角色移动的,我们就先来利用横向卷轴实现背景的移动吧。
其原理很简单,就是两个相同的背景图像,并在一起。为了方便理解称左边的为A,右边的为B,A与B一起滚动,当A已经滚过摄像头时,让A赶紧跟在B的后面,继续参与滚动。
那么我们复制粘贴一下我们的背景出来。
调整之后,当A的水平坐标处于0,B的水平坐标处于40时,摄像机只有A的画面,A与B无缝衔接。那么说明在滚动过程中,如果A的水平坐标为-40,那么B的水平坐标就一定为0,这时间摄像机的画面是B,赶紧把A的坐标更改为40,那么前后两者之间的差依然是40,依然无缝衔接,我们简单的卷轴滚动就做好了。
实现的脚本如下。
public class Background_Roll : MonoBehaviour
{public float rollSpeed = 10.0f;void Update(){if (transform.position.x <= -40){transform.position = new Vector3(40, transform.position.y, transform.position.z);}transform.Translate(Vector2.left * rollSpeed * Time.deltaTime);}
}
当然平台的滚动也可以这样写,但是平台可能涉及到角色碰撞等因素,最好通过移动刚体来实现。这样的话记住不能冻结X轴上的位置,也要排除Platform层防止平台间发生碰撞。
public class Platform_Roll : MonoBehaviour
{public float rollSpeed = 10.0f;//因为涉及到碰撞器更新,使用需要移动刚体Rigidbody2D rb;void Start(){rb = GetComponent<Rigidbody2D>();}void Update(){if (transform.position.x <= -40){transform.position = new Vector3(40, transform.position.y, transform.position.z);}}void FixedUpdate(){rb.MovePosition(rb.position + Vector2.left * rollSpeed * Time.fixedDeltaTime);}
}
现在再来看看我们角色跑动的效果。
发现角色动不动就给我跳一下。这是因为A与B平台的碰撞器是分开,当离开一个平台的碰撞器时,就会触发跳跃。解决方法也十分简单,加入碰撞体的持续检测即可。
private void OnCollisionStay2D(Collision2D collision)
{if (collision.gameObject.layer == LayerMask.NameToLayer("Platform")){onPlatform = true;animator.SetBool("isJump", false);}
}
可以看到基本的效果已经有了。
生成障碍物:
现在该考虑角色要靠跳跃才能越过的障碍物了。
这里选择的是素材中的Mace与Saw图像。那么我们先来调整这两个障碍物的预制件,让他们以更动态的方式生成在场景中,便于后续随机生成。将Saw拖入场景,首先我们在资产下创建一个名为Saw的动画,用来编辑这个锯齿的动画,并将其附在场景中的Saw上,并同时生成了动画控制器。
双击该动画资产,就可以进行编辑了。我们点击右下角的“曲线”,一般默认的动画编辑数值改变是曲线的,我们让我们的锯齿持续转动我们需要设置为线性。然后在动画中添加Saw的旋转属性,将关键帧添加在30帧的位置,Z轴设置为360度,同下图一样设置为线性。
记住在检查其中将循环时间选中,这样我们的齿轮就可以转动起来了。
然后以同样的方式,为我们的Mace添加一上一下的动态效果。
但要注意,动画系统中位置的动画不能单独对一个轴编辑,所以为了防止动画系统对我们后来的物理系统产生影响,我们为Mace创建一个父物体,在后续,不管是制成预制件还是添加物理逻辑,都对这个父物体操作。
为二者添加合适的碰撞器以及刚体,冻结y轴位置以及旋转,排除Platform层,同时将这二者的标签更改为“Obstacle”,以便后续逻辑的进行。然后将这二者拖入资产当中作为预制件。然后可以将场景中的两个物体删掉了。
现在考虑如何将障碍物生成在场景中。这里使用C#中的标准队列结构将这两个预制件存放在对象池中,通过其先进先出的特性让物体能够回收利用,避免多次销毁,增加性能。将生成点设置在摄像机范围右侧,在生成的已经存在的物体消失后,生成下一个物体,达到效果。
创建一个名为“SpawnPoint”的空物体用来确定生成障碍物的位置,编写以下脚本挂载到这个生成点上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawn_Obstacle : MonoBehaviour
{public GameObject obstacle1;public GameObject obstacle2;GameObject current;public float rollSpeed = 10.0f;//因为涉及到碰撞器更新,使用需要移动刚体Rigidbody2D rb;//创建对象池public Queue<GameObject> pool = new Queue<GameObject>();//初始化对象池void InitializePool(){//在池中创建六个对象for (int i = 0; i < 3; i++){GameObject obj1 = Instantiate(obstacle1);obj1.SetActive(false);pool.Enqueue(obj1);//放入队列GameObject obj2 = Instantiate(obstacle2);obj2.SetActive(false);pool.Enqueue(obj2);//放入队列}}//从对象池里获取对象public GameObject GetObject(){return pool.Dequeue();//娶一个队列里的元素}//回收对象public void ReturnObject(GameObject obj){obj.SetActive(false);pool.Enqueue(obj);}void Start(){InitializePool();current = GetObject();current.SetActive(true);rb = current.GetComponent<Rigidbody2D>();//让物体的位置在我们设置的生成点的位置rb.position = transform.position;}void Update(){//-20为摄像机范围左侧的少许距离if (current.transform.position.x <= -20){ReturnObject(current);current = GetObject();current.SetActive(true);rb = current.GetComponent<Rigidbody2D>();//让物体的位置在我们设置的生成点的位置rb.position = transform.position;}}private void FixedUpdate(){rb.MovePosition(rb.position + Vector2.left * rollSpeed * Time.fixedDeltaTime);}
}
将预制件拖入脚本组件,我们的障碍物的基本行为就做好了。
为了更便于角色撞到障碍物就死掉,我们将障碍物的碰撞器都设置为触发器。我们准备好角色的死亡动画,开始添加角色的死亡逻辑。注意这里的过渡不要选可以过渡到自己,会发生错误。
public class Player_Die : MonoBehaviour
{Animator animator;void Awake(){animator = GetComponent<Animator>();}private void OnTriggerEnter2D(Collider2D collision){if (collision.gameObject.CompareTag("Obstacle")){animator.SetBool("die", true);}}
}
现在我们的角色死亡也就安排好了。
生成敌人与攻击逻辑:
下一步要生成敌人了,为了简单起见,我们将敌人的图像同上文中的Mace一样,父物体添加碰撞器刚体,制为预制件,将标签改为“Monster”,因为原资产的文件怪物是朝左的,我们我们选择反转x轴。
然后在原本生成障碍物的脚本里添加几行,好让生成点交替生成怪物。
public GameObject obstacle1;public GameObject obstacle2;public GameObject monster;void InitializePool(){//在池中创建六个对象for (int i = 0; i < 2; i++){GameObject obj1 = Instantiate(obstacle1);obj1.SetActive(false);pool.Enqueue(obj1);//放入队列GameObject obj2 = Instantiate(obstacle2);obj2.SetActive(false);pool.Enqueue(obj2);//放入队列GameObject obj3 = Instantiate(monster);obj3.SetActive(false);pool.Enqueue(obj3);//放入队列}}
然后修改角色死亡的逻辑。
private void OnTriggerEnter2D(Collider2D collision)
{if (collision.gameObject.CompareTag("Obstacle") || collision.gameObject.CompareTag("Monster")){animator.SetBool("die", true);}
}
现在怪物就会在两个障碍物后出现。
现在我们考虑角色的攻击。毕竟我们手上没有现成的素材,不如让我们搓一个螺旋丸吧。创建一个空物体,放置在角色前方。在其下随意制作我们的武器,这里我随便搓了一个,并加上了和锯齿同款的旋转。
为这个武器添加名为“Weapon”的标签,用于处理后续攻击怪物的逻辑,添加圆形碰撞器。然后为怪物预制件添加如下脚本,记得加上怪物的死亡动画。
public class Monster_Die : MonoBehaviour
{Animator animator;void Awake(){//因为要挂载到父物体上,所以要获取子物体的动画器animator = GetComponentInChildren<Animator>();}private void OnTriggerEnter2D(Collider2D collision){if (collision.gameObject.CompareTag("Weapon")){animator.SetBool("die", true);//需要在生成点脚本中重新使其恢复Collider2D collider = GetComponent<Collider2D>();collider.enabled = false;}}
}
这样一来,我们的螺旋丸就可以将怪物击倒了。然后我们更改角色控制脚本,编写使用武器攻击的逻辑。
public GameObject weapon;
IEnumerator Attack()
{animator.SetBool("isAttack", true);weapon.SetActive(true);yield return new WaitForSeconds(0.8f);animator.SetBool("isAttack", false);weapon.SetActive(false);//等等一段时间后才能进行下一次攻击,相当于冷却时间yield return new WaitForSeconds(0.5f);isAttack = false;
}
现在我们攻击怪物的逻辑就做好了。
UI制作与重新开始:
现在我们基本的游戏内容就已经做好了,也该进入收尾阶段了,现在就该讲讲如何在角色死亡后如何重置我们的场景以及GUI布置。
先来实现重新开始的逻辑,因为我们这个简陋的小游戏并不需要保存状态什么的需求,所以直接通过重置整个场景即可。我们在场景中创建一个名为“GameManager”的空物体,专门用来挂载需要对游戏全局控制的脚本。然后挂载以下脚本。
public class GameManager : MonoBehaviour
{public void ResetScene(){//等一秒再重新开始StartCoroutine(ReloadWithFade());}IEnumerator ReloadWithFade(){yield return new WaitForSeconds(1f);SceneManager.LoadScene(SceneManager.GetActiveScene().name);}
}
由于我们的角色是和游戏结束关联的,所以需要在角色死亡脚本中添加我们GameManager类的实例。保存后将场景GameManager游戏对象拖入该脚本组件即可。
public class Player_Die : MonoBehaviour
{private void OnTriggerEnter2D(Collider2D collision){if (collision.gameObject.CompareTag("Obstacle") || collision.gameObject.CompareTag("Monster")){animator.SetBool("die", true);//游戏结束gameManager.ResetScene();}}
}
现在我们就完成了极其简单的场景重置。如果想要添加什么效果(如淡入淡出)可以直接在游戏管理脚本中的协程中添加。
现在开始添加GUI,我想让我们游戏一开始,角色处于待机状态,场景也没有变化,需要点击“开始游戏”以后才能够让场景与各种对象进行移动。那么将思路转换为具体方法,就是角色刚进入游戏时,需要禁用背景滚动脚本、平台滚动脚本、角色控制脚本,以及生成点脚本。
为了批量控制这些脚本,我们将所有目标脚本所挂载的游戏对象的标签更改为“ScriptControl”,然后修改GameManager脚本,禁用所有脚本,具体如下。
public class GameManager : MonoBehaviour
{GameObject[] targets;private void Awake(){//初始化所有的带有标签的物体targets = GameObject.FindGameObjectsWithTag("ScriptControl");DisableScripts();}//批量禁用脚本public void DisableScripts(){foreach (GameObject obj in targets){//禁用所有继承自MonoBehaviour脚本MonoBehaviour[] scripts = obj.GetComponents<MonoBehaviour>();foreach (MonoBehaviour script in scripts){if (script != null && script.enabled){script.enabled = false;//这个方法可以用来Debug是否真的禁用了Debug.Log($"已禁用 {obj.name} 上的 {script.GetType().Name}");}}}}public void ResetScene(){//等一秒再重新开始StartCoroutine(ReloadWithFade());}IEnumerator ReloadWithFade(){yield return new WaitForSeconds(1f);SceneManager.LoadScene(SceneManager.GetActiveScene().name);}
}
虽然这时检查我们的脚本确实被禁用了,但是会发现我们的控制台会持续出现报错。
查阅后得知,可以是因为OnCollisionStay2D等方法是Unity物理系统的回调,不受脚本启用状态影响。所以我们要同时禁用他们的碰撞器与刚体。
//批量禁用碰撞器和刚体public void DisableColliders(){foreach (GameObject obj in targets){//禁用所有碰撞器和刚体(防止人物掉落)if (obj.GetComponent<Collider2D>()){Rigidbody2D[] rbs = obj.GetComponents<Rigidbody2D>();foreach (Rigidbody2D rb in rbs){if (rb != null && rb.simulated){//Rigidbody2D特有的方法,用来定义刚体是否参与物理模拟rb.simulated = false;//这个方法可以用来Debug是否真的禁用了Debug.Log($"已禁用 {obj.name} 上的 {rb.GetType().Name}");}}Collider2D[] colliders = obj.GetComponents<Collider2D>();foreach (Collider2D collider in colliders){if (collider != null && collider.enabled){collider.enabled = false;//这个方法可以用来Debug是否真的禁用了Debug.Log($"已禁用 {obj.name} 上的 {collider.GetType().Name}");}}}}}
然后我们再将角色一开始的动画设置为“闲置”,即创建默认过渡为idel动画。
那么我们游戏一开始的场景就布置完成了。
现在就在我们的右半边屏幕布设GUI。在场景中创建一块画布。选择屏幕空间覆盖渲染,按屏幕大小缩放。在这个物体下创建文本,用来写我们的标题,字体格式用我们导入的资源。同时创建一个按钮,这个用来让我们开始游戏。
现在我们来编写按钮按下后触发的逻辑,那就是让我们刚刚禁用的脚本还有碰撞器刚体什么的统统恢复原状,还有要想之前一样,让角色的动画过渡到奔跑动画。对我们的GameManager脚本作如下修改。
//批量激活脚本public void EnableScripts(){foreach (GameObject obj in targets){//禁用所有继承自MonoBehaviour脚本MonoBehaviour[] scripts = obj.GetComponents<MonoBehaviour>();foreach (MonoBehaviour script in scripts){if (script != null && !script.enabled){script.enabled = true;}}}}//批量激活碰撞器和刚体public void EnableColliders(){foreach (GameObject obj in targets){//禁用所有碰撞器和刚体(防止人物掉落)if (obj.GetComponent<Collider2D>()){//先激活碰撞器Collider2D[] colliders = obj.GetComponents<Collider2D>();foreach (Collider2D collider in colliders){if (collider != null && !collider.enabled){collider.enabled = true;}}Rigidbody2D[] rbs = obj.GetComponents<Rigidbody2D>();foreach (Rigidbody2D rb in rbs){if (rb != null && !rb.simulated){//Rigidbody2D特有的方法,用来定义刚体是否参与物理模拟rb.simulated = true;}}}}}
然后在角色控制脚本中,添加如下代码。
public void startRun(){animator = GetComponent<Animator>();animator.SetBool("start", true);}
然后再在我们的按钮组件中,依次使用这些方法,再将画布禁用,好让我们的玩家回到游戏状态。
哦,不能忘了告诉玩家操作方法。为了手感舒适一点,我将跳跃改为了W键,将滑板车改为了S键,攻击改为了D键。将这些信息写在我们的UI界面中。
那么我们禁用脚本的方法当然也不是白写的,在角色死亡之后,可以让场景中的物体都停止移动一段时间,再加载我们的新场景。对加载新场景的协程作如下更改。
IEnumerator ReloadWithFade()
{DisableColliders();DisableScripts();yield return new WaitForSeconds(2f);SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
那么到现在我们的跑酷小游戏就大功告成了!
导出游戏:
终于到了激动人心的导出游戏时刻。点击生成设置。
在“玩家设置“中调整分辨率等参数后(一般默认就行),点击构建和运行,我们的游戏就到处完成了。
小结
当然,这款小游戏还有许多许多可以优化的地方,这种多层背景也可以做出像《空洞骑士》那样的景深效果,更有立体感;经典的设置生命值增加受伤次数,增加容错率;在到达一定时间后可以将场景切换为别的,更有新鲜感;在屏幕上方显示这一次跑酷坚持了多长时间,更有成就感;让障碍物与敌人随机生成,更有可玩性;甚至可以加入bgm,更具舒适感……这些方法的实践,我会陆续写在后续的开发笔记中,也会在github中同步更新,感兴趣的话请多多关注。
如有补充纠正,欢迎留言。
相关文章:
Unity2D:从零开始制作一款跑酷游戏!
目录 成品展示 美术资源 制作步骤 场景预布设: 实现人物基础功能: 移动背景——横向卷轴: 生成障碍物: 生成敌人与攻击逻辑: UI制作与重新开始: 导出游戏: 小结 大家小时候都玩过《…...
维拉工时自定义字段:赋能项目数据的深度洞察 | 上新预告
原文链接:维拉工时自定义字段:赋能项目数据的深度洞察 | 上新预告 在项目管理实践中,每个企业都有独特的数据统计与分析需求。为了帮助用户实现个性化数据建模,从而更精准地衡量项目进度和预算投入,维拉工时全新升级的…...
C++ | 文件读写(ofstream/ifstream/fstream)
一、C文件操作核心类 C标准库通过<fstream>提供了强大的文件操作支持,主要包含三个关键类: 类名描述典型用途ofstream输出文件流(Output File Stream)文件写入操作ifstream输入文件流(Input File Stream&#…...
flux文生图部署笔记
目录 依赖库: 文生图推理代码cpu: cuda版推理: 依赖库: tensorrt安装: pip install nvidia-pyindex # 添加NVIDIA仓库索引 pip install tensorrt 文生图推理代码cpu: import torch from diffusers import FluxPipelinemodel_id = "black-forest-labs/FLUX.1-s…...
二语习得理论(Second Language Acquisition, SLA)如何学习英语
二语习得理论(Second Language Acquisition, SLA)是研究学习者如何在成人或青少年阶段学习第二语言(L2)的理论框架。该理论主要关注语言习得过程中的认知、社会和文化因素,解释了学习者如何从初学者逐渐变得流利并能够…...
策略模式实际用处,改吧改吧直接用,两种方式
controller RestController RequestMapping("admin/test") RequiredArgsConstructor(onConstructor __(Autowired)) public class TestController {Autowiredprivate VideoFactory VideoFactory;GetMapping("getList")public R getList(){// 第一种方式T…...
计算机网络-TCP的流量控制
内容来源:小林coding 本文是对小林coding的TPC流量控制的精简总结 什么是流量控制 发送方不能无脑的发数据给接收方,要考虑接收方处理能力 如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制 从而导致网…...
搬砖--贪心+排序的背包
a在上面b在下面->a.v-M-b.m>b.v-M-a.m->剩余率大 所以我先遍历a,让a在上面 这就是要考虑贪心排序的01背包 因为它有放的限制条件 #include<bits/stdc.h> using namespace std; #define N 100011 typedef long long ll; typedef pair<ll,int>…...
git克隆数据失败
场景:当新到一家公司,然后接手了上一个同时的电脑,使用git克隆代码一直提示无法访问,如图 原因:即使配置的新的用户信息。但是window记录了上一个同事的登录信息,上一个同事已经被剔除权限,再拉…...
Dart 语法
1. 级联操作符 … var paint Paint()..color Colors.black..strokeCap StrokeCap.round..strokeWidth 5.0;2. firstWhereOrNull 3. 隐藏或导入部分组件 // Import only foo. import package:lib1/lib1.dart show foo;// Import all names EXCEPT foo. import package:lib…...
34、web前端开发之JavaScript(三)
十. DOM操作详解 1、DOM简介 文档对象模型(DOM,Document Object Model)是JavaScript与网页内容交互的接口。它将HTML文档表示为一种树状结构(DOM树),其中每个节点代表文档的一部分(例如元素、…...
自适应卡尔曼滤波
目录 自适应卡尔曼滤波 自适应卡尔曼滤波 主要作用,去抖动 AdaptiveKalmanFilter.py import cv2 import numpy as np from collections import deque# ------------------ 核心去抖算法 ------------------ import cv2 import numpy as np from collections import deque#…...
EIP-712:类型化结构化数据的哈希与签名
1. 引言 以太坊 EIP-712: 类型化结构化数据的哈希与签名,是一种用于对类型化结构化数据(而不仅仅是字节串)进行哈希和签名 的标准。 其包括: 编码函数正确性的理论框架,类似于 Solidity 结构体并兼容的结构化数据规…...
Day 3:Leetcode 比特位计数+只出现一次的数字 II
比特位计数 本质是一个递推,时间复杂度O(n),空间复杂度O(n)。 class Solution { public:vector<int> countBits(int n) {vector<int> ans(n 1);ans[0] 0;//ans[1] 1;for(int i 1; i < n; i){if(i & 1){ans[i] ans[i/2] 1;}else…...
CentOS7安装conda
root用户登录虚拟机后更新yum yum update 下载Miniconda,路径就在/root下 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh conda分为Miniconda和Anaconda, Miniconda是Conda的轻量级版本,只包含conda和Python…...
[Linux]从零开始的vs code交叉调试arm Linux程序教程
一、前言 最近的项目中需要集成rknn的视觉识别,在这之前我并且没有将rknn集成到自己项目的经验。这里我需要在rknn原本demo的基础上我还需要集成自己的业务代码。但是又有一个问题,原本rknn我们都是使用交叉编译编译到开发板上的,并且我们还要…...
【顶刊级科研绘图】AI支持下Nature级数据可视化(如何画图、如何标注、如何改图、如何美化、如何组合、如何排序)
技术点目录 第一章、绘图原则与概念:规范清晰简洁自明第二章、DeepSeek、ChatGPT、R绘图系统:八仙过海各显神通第三章、美学设计与细节:完美图表华丽呈现第四章、数据类型与图表:宝典在手各个击破第五章、统计分析与可视化&#x…...
CSRF跨站请求伪造——入门篇【DVWA靶场low级别writeup】
CSRF跨站请求伪造——入门篇 0. 前言1. 什么是CSRF2. 一次完整的CSRF攻击 0. 前言 本文将带你实现一次完整的CSRF攻击,内容较为基础。需要你掌握的基础知识有: 了解cookie;已经安装了DVWA的靶场环境(本地的或云的)&am…...
Spring Boot应用中实现Jar包热更新的实践指南
Spring Boot应用中实现Jar包热更新的实践指南 一、引言 在现代软件开发中,快速迭代和持续交付是至关重要的。对于基于Spring Boot的应用程序,一旦部署到生产环境,传统的更新方式通常是重新打包并重启应用,这不仅耗时,…...
JVM深入原理(七)(一):运行时数据区
目录 7. JVM运行时数据区 7.1. 运行时数据区-总览 7.2. 运行时数据区-查看内存对象 7.3. 运行时数据区-程序计数器 7.3.1. 程序计数器-作用 7.3.2. 字节码指令执行流程 7.4. 运行时数据区-Java虚拟机栈 7.4.1. 栈-概述 7.4.2. 栈帧-组成 7.4.2.1. 栈帧-帧数据 7.4.2…...
约瑟夫环的四种(数组,链表,递归,迭代)解决方案,与空间、时间复杂度分析
以下方法均没有考虑结果集的空间与时间复杂度 1.数组解法 实现代码(未优化) class Main {public static void main(String[] args){Scanner read new Scanner(System.in);int n read.nextInt();int m read.nextInt();int[] people new int[n]; //…...
skynet.start 的作用详细解析
目录 skynet.start 的作用详细解析1. 功能概述2. 基本用法3. 关键作用(1) 注册消息处理函数(2) 启动事件循环(3) 服务生命周期管理 4. 与其他函数的协作5. 未调用 skynet.start 的后果6. 高级场景:何时不需要 skynet.start7. 总结 skynet.start 的作用详细解析 在 …...
Apache Doris 2025 Roadmap:构建 GenAI 时代实时高效统一的数据底座
在全球 290 位开发者的协作下,Apache Doris 在 2024 年完成了 7000 次代码提交,并发布了 22 个版本,实现在实时分析、湖仓一体和半结构化数据分析等核心场景的技术突破及创新。 2025 年,Apache Doris 社区将秉承“以场景驱动创新…...
springboot+easyexcel实现下载excels模板下拉选择
定义下拉注解 Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface ExcelDropDown {/*** 固定下拉选项*/String[] source() default {};/*** 动态数据源key(从上下文中获取)*/String sourceMethod() default "";…...
vue3+ts+element-plus 开发一个页面模块的详细过程
目录、文件名均使用kebab-case(短横线分隔式)命名规范 子组件目录:./progress-ctrl/comps 1、新建页面文件 progress-ctrl.vue <script setup lang"ts" name"progress-ctrl"></script><template>&l…...
软考《信息系统运行管理员》- 7.1 物联网运维
物联网的概念及特征 物联网是在计算机互联网的基础上,通过射频识别 (RFID) 、 无线传感器、红外感应器、 全球定位系统、激光扫描器等信息传感设备,按约定的协议,把物与物之间通过网络连接起来, 进行信息交换和通信,以…...
【GPT入门】第33 课 一文吃透 LangChain:chain 结合 with_fallbacks ([]) 的实战指南
[TOC](【GPT入门】第33课 一文吃透 LangChain:chain 结合 with_fallbacks ([]) 的实战指南) 1. fallback概述 模型回退,可以设置在llm上,也可以设置在chain上,都带有with_fallbacks([])函数 2. llm的回退 2.1 代码 核心代码&…...
裴蜀定理:整数解的奥秘
裴蜀定理:整数解的奥秘 在数学的世界里,裴蜀定理(Bzout’s Theorem)是数论中一个非常重要的定理,它揭示了二次方程和整数解之间的关系。它不仅仅是纯粹的理论知识,还在计算机科学、密码学、算法优化等多个…...
Table as Thought论文精读
标题:Table as Thought: Exploring Structured Thoughts in LLM Reasoning 作者:Zhenjie Sun, Naihao Deng, Haofei Yu, Jiaxuan You 单位:University of Illinois Urbana-Champaign, University of Michigan 摘要: llm的推理…...
PyQt6实例_A股日数据维护工具_使用
目录 前置: 下载预备更新的数据 使用工具更新 用工具下载未复权、前复权、权息数据 在PostgreSQL添加两个数据表 工具&视频 前置: 1 本系列将以 “PyQt6实例_A股日数据维护工具” 开头放置在“PyQt6实例”专栏 2 日数据可在“数据库”专栏&…...
MySQL客户端工具-图形化工具-DataGrip 安装与使用
一. 常见的图形化工具 二. DataGrip 安装 官网:DataGrip:由 JetBrains 开发的数据库和 SQL 跨平台 IDE 二. DataGrip 使用...
企业管理系统的功能架构设计与实现
一、企业管理系统的核心功能模块 企业管理系统作为现代企业的中枢神经系统,涵盖了多个核心功能模块,以确保企业运营的顺畅与高效。这些功能模块通常包括: 人力资源管理模块:负责员工信息的录入、维护、查询及统计分析,…...
1.Qt信号与槽
本篇主要介绍信号和槽,如何关联信号和槽以及用QPixmap在窗口中自适应显示图片 本文部分ppt、视频截图原链接:[萌马工作室的个人空间-萌马工作室个人主页-哔哩哔哩视频] 1. 信号 一般不需要主动发送信号,只有自定义的一些控件才需要做信号的…...
再生认证体系有哪些?不同标准对应的要求及可以做的审核机构
再生认证体系 标准 GRS再生回收认证要求 再生原材料的上游企业:需要具备GRS认证证书,以确保原材料的可追溯性和再生成分。 认证条件: 最终商品的再生成分比例必须至少为20%。 只有由至少50%的回收材料制成的产品才能贴上GRS标签。 认证机构…...
[CISSP] [6] 密码学和对称密钥算法
密码学的目标 1. 机密性(Confidentiality) 目标:保护信息不被未授权访问。 通过 加密(Encryption)技术确保数据只能被授权方解密和读取。主要方法: 对称加密(AES、3DES)ÿ…...
thinkphp每条一级栏目中可自定义添加多条二级栏目,每条二级栏目包含多个字段信息
小程序客户端需要展示团购详情这种结构的内容,后台会新增多条套餐,每条套餐可以新增多条菜品信息,每条菜品信息包含菜品名称,价格,份数等字段信息,类似于购物网的商品多规格属性,数据表中以json类型存储,手写了一个后台添加和编辑的demo 添加页面 编辑页面(json数据…...
混杂模式(Promiscuous Mode)与 Trunk 端口的区别详解
一、混杂模式(Promiscuous Mode) 1. 定义与工作原理 定义:混杂模式是网络接口的一种工作模式,允许接口接收通过其物理链路的所有数据包,而不仅是目标地址为本机的数据包。工作层级:OSI 数据链路层&#x…...
Spring Boot项目信创国产化适配指南
将 Spring Boot 项目适配信创国产化环境,需要从底层基础设施到上层应用组件进行全面替换和调整。以下是主要替换点和适配步骤的总结: 一、基础软件替换 1. JDK 替换 国外JDK:Oracle JDK、OpenJDK国产JDK: 阿里龙井(D…...
MySQL:数据类型
数值类型 数值类型用于存储整数、小数、浮点数等,主要分为整数类型和浮点类型。 整数类型 数据类型存储大小取值范围(有符号)取值范围(无符号)说明TINYINT1字节-128 ~ 1270 ~ 255小整数,如布尔值&#x…...
maven引入项目内本地包方法
最近在写java实现excel转pdf功能; 网上有个包很好用,免费:spire.xls.free-5.3.0.jar。 但是maven打包项目时报错,找不到这个包。 jar包位置如下: 在项目/src/jar/spire.xls.free-5.3.0.jar。 解决方法:…...
ARP协议
ARP协议 ARP协议的作用 当网络设备有数据要发送给另一台网络设备时,必须要知道对方的网络层地址(即IP地址)。IP地址由网络层来提供,但是仅有IP地址是不够的,IP数据报文必须封装成帧才能通过数据链路进行发送。数据帧…...
科技赋能安居梦:中建海龙以模块化革新重塑城市更新范式
在北京市西城区桦皮厂胡同,一栋始建于上世纪70年代的住宅楼正经历着一场脱胎换骨的蜕变。这座曾被鉴定为D级危房的建筑,在中建海龙科技有限公司(以下简称“中建海龙”)的匠心打造下,仅用三个月便完成"原拆原建&qu…...
2025 AI智能数字农业研讨会在苏州启幕,科技助农与数据兴业成焦点
4月2日,以"科技助农数据兴业”为主题的2025AI智能数字农业研讨会在苏州国际博览中心盛大启幕。本次盛会吸引了来自全国各地相关部门领导、知名专家学者、行业协会组织,以及县级市农业企业代表、县级市农产品销售商等万名嘉宾齐聚姑苏城,…...
2000-2021年 全国各地区城镇登记失业率数据
全国各地区城镇登记失业率数据2000-2021年.ziphttps://download.csdn.net/download/2401_84585615/90259723 https://download.csdn.net/download/2401_84585615/90259723 城镇登记失业率是衡量地区就业状况的重要指标,反映了在一定时期内,符合就业条件的…...
Cursor的主要好处
以下是Cursor的主要好处: 代码生成与优化 • 快速生成代码:根据简短描述或部分代码片段,Cursor能快速生成完整代码模块,还能智能预测下一步操作,将光标放在合适位置,让开发者一路Tab键顺滑编写代码。 • …...
超便捷语音转文字工具CapsWriter-Offline本地部署与远程使用全流程
文章目录 前言1. 软件与模型下载2. 本地使用测试3. 异地远程使用3.1 内网穿透工具下载安装3.2 配置公网地址3.3 修改config文件3.4 异地远程访问服务端 4. 配置固定公网地址4.1 修改config文件 5. 固定tcp公网地址远程访问服务端 前言 今天给大家安利一个绝对能让你工作效率飙…...
什么是数据仓库
什么是数据仓库 Data warehouse 是面向主题的 主要根据各种数据来源,来进行历史分析 形成一个趋势分析 为数据挖掘、预测建模、机器学习提供基础数据 与传统数据库比如gaussdb的区别。数据仓库注重历史数据分析,guassdb注重实时事务处理 数据仓库时企业的…...
【动态规划】二分优化最长上升子序列
最长上升子序列 II 题解 题目传送门:AcWing 896. 最长上升子序列 II 一、题目描述 给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。 输入格式: 第一行包含整数 N第二行包含 N 个整数,表示完整序列 输…...
MySQL的安装与初始化流程
MySQL概述 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,MySQL AB公司被Sun公司收购,Sun公司又被Oracle公司收购,目前属于Oracle公司。 MySQL是目前最流行的关系型数据库管理系统,在WEB应用方面MySQL是最…...
flink standalone集群模式部署
一. 环境准备 1、下载并安装jdk11 2、下载flink 并解压 3、确保服务器之间的免密登录 二、集群搭建 搭建集群至少有三台机器,每台机器的分配角色如下 master: jobManager salve01:taskManager salve02:taskManager 1、在JobManager(…...