这样就成功的绘制了一个圆形,我们在这把它当作一个小球:
不过,这个时候的小球还是静止的,如果想让它移动,那么得修改它的圆心坐标,具体修改的数值则与运动速度有关。在移动小球之前,先看一下 canvas 进行动画的原理:
Canvas 进行动画的原理与传统的电影胶片类似,在一段时间内,绘制图像、更新图像位置或形状、清除画布,重新绘制图像,当在 1 秒内连续执行 60 次或以上这样的操作时,即以 60 帧的速度,就可以产生连续的画面。
那么在 JavaScript 中,浏览器提供了 window.requestAnimationFrame()
方法,它接收一个回调函数作为参数,每一次执行回调函数就相当于 1 帧动画,我们需要通过递归或循环连续调用它,浏览器会尽可能的在 1 秒内执行 60 次回调函数。那么利用它,我们就可以对 canvas 进行重绘,以实现小球的移动效果。
由于
window.requestAnimationFrame()
的调用基本是持续进行的,所以我们也可以把它称为游戏循环(Game loop)。
接下来我们来看如何编写动画的基础结构:
function process() {
window.requestAnimationFrame(process);
}
window.requestAnimationFrame(process);
这里的 process()
函数就是 1 秒钟要执行 60 次的回调函数,每次执行完毕后继续调用 window.requestAnimationFrame(process)
进行下一次循环。如果要移动小球,那么就需要把绘制小球和修改圆心 x、y 坐标的代码写到 process()
函数中。
为了方便更新坐标,我们把小球的圆心坐标保存到变量中,以方便对它们进行修改,然后再定义两个新的变量,分别表示在 x 轴方向上的速度 vx
,和 y 轴方向上的速度 vy
,然后把 context 相关的绘图操作放到 process()
中:
let x = 100;
let y = 100;
let vx = 12;
let vy = 25;
function process() {
ctx.fillStyle = “hsl(170, 100%, 50%)”;
ctx.beginPath();
ctx.arc(x, y, 60, 0, 2 * Math.PI);
ctx.fill();
window.requestAnimationFrame(process);
}
window.requestAnimationFrame(process);
要计算圆心坐标 x、y 的移动距离,我们需要速度和时间,速度这里有了, 那么时间要怎么获取呢? window.requestAnimationFrame()
会把当前时间的毫秒数(即时间戳)传递给回调函数,我们可以把本次调用的时间戳保存起来,然后在下一次调用时计算出执行这 1 帧动画消耗了多少秒,然后根据这个秒数和 x、y 轴方向上的速度去计算移动距离,分别加到 x 和 y 上,以获得最新的位置。注意这里的时间是上一次函数调用和本次函数调用的时间间隔,并不是第 1 次函数调用到当前函数调用总共过去了多少秒,所以相当于是时间增量,需要在之前 x 和 y 的值的基础上进行相加,代码如下:
let startTime;
function process(now) {
if (!startTime) {
startTime = now;
}
let seconds = (now - startTime) / 1000;
startTime = now;
// 更新位置
x += vx * seconds;
y += vy * seconds;
// 清除画布
ctx.clearRect(0, 0, width, height);
// 绘制小球
ctx.fillStyle = “hsl(170, 100%, 50%)”;
ctx.beginPath();
ctx.arc(x, y, 60, 0, 2 * Math.PI);
ctx.fill();
window.requestAnimationFrame(process);
}
process()
现在接收当前时间戳作为参数,然后做了下面这些操作:
-
计算上次函数调用与本次函数调用的时间间隔,以秒计,记录本次调用的时间戳用于下一次计算。
-
根据 x、y 方向上的速度,和刚刚计算出来的时间,计算出移动距离。
-
调用
clearRect()
清除矩形区域画布,这里的参数,前两个是左上角坐标,后两个是宽高,把 canvas 的宽高传进去就会把整个画布清除。 -
重新绘制小球。
现在小球就可以移动了:
上边的代码适合只有一个小球的情况,如果有多个小球需要绘制,就得编写大量重复的代码,这时我们可以把小球抽象成一个类,里边有绘图、更新位置等操作,还有坐标、速度、半径等属性,重构后的代码如下:
class Circle {
constructor(context, x, y, r, vx, vy) {
this.context = context;
this.x = x;
this.y = y;
this.r = r;
this.vx = vx;
this.vy = vy;
}
// 绘制小球
draw() {
this.context.fillStyle = “hsl(170, 100%, 50%)”;
this.context.beginPath();
this.context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
this.context.fill();
}
/**
-
更新画布
-
@param {number} seconds
*/
update(seconds) {
this.x += this.vx * seconds;
this.y += this.vy * seconds;
}
}
里边的代码跟之前的一样,这里就不再赘述了,需要注意的是,Circle 类的 context 画笔属性是通过构造函数传递进来的,更新位置的代码放到了 update()
方法中。
对于整个 canvas 的绘制过程,也可以抽象成一个类,当作是游戏或引擎控制器,例如把它放到一个叫 Gameboard
的类中:
class Gameboard {
constructor() {
this.startTime;
this.init();
}
init() {
this.circles = [
new Circle(ctx, 100, 100, 60, 12, 25),
new Circle(ctx, 180, 180, 30, 70, 45),
];
window.requestAnimationFrame(this.process.bind(this));
}
process(now) {
if (!this.startTime) {
this.startTime = now;
}
let seconds = (now - this.startTime) / 1000;
this.startTime = now;
for (let i = 0; i < this.circles.length; i++) {
this.circles[i].update(seconds);
}
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < this.circles.length; i++) {
this.circles[i].draw(ctx);
}
window.requestAnimationFrame(this.process.bind(this));
}
}
new Gameboard();
在 Gameboard 类中:
-
startTime
保存了上次函数执行的时间戳的属性,放到了构造函数中。 -
init()
方法创建了一个circles
数组,里边放了两个示例的小球,这里先不涉及碰撞问题。然后调用window.requestAnimationFrame()
开启动画。注意这里使用了bind()
来把Gameboard
的 this 绑定到回调函数中,以便于访问Gameboard
中的方法和属性。 -
process()
方法也写到了这里边,每次执行时会遍历小球数组,对每个小球进行位置更新,然后清除画布,再重新绘制每个小球。 -
最后初始化
Gameboard
对象就可以开始执行动画了。
这个时候有两个小球在移动了。
为了实现仿真的物理特性,多个物体间碰撞会有相应的反应,第一步就是要先检测碰撞。我们先再多加几个小球,以便于碰撞的发生,在 Gameboard 类的 init()
方法中再添加几个小球:
this.circles = [
new Circle(ctx, 30, 50, 30, -100, 390),
new Circle(ctx, 60, 180, 20, 180, -275),
new Circle(ctx, 120, 100, 60, 120, 262),
new Circle(ctx, 150, 180, 10, -130, 138),
new Circle(ctx, 190, 210, 10, 138, -280),
new Circle(ctx, 220, 240, 10, 142, 350),
new Circle(ctx, 100, 260, 10, 135, -460),
new Circle(ctx, 120, 285, 10, -165, 370),
new Circle(ctx, 140, 290, 10, 125, 230),
new Circle(ctx, 160, 380, 10, -175, -180),
new Circle(ctx, 180, 310, 10, 115, 440),
new Circle(ctx, 100, 310, 10, -195, -325),
new Circle(ctx, 60, 150, 10, -138, 420),
new Circle(ctx, 70, 430, 45, 135, -230),
new Circle(ctx, 250, 290, 40, -140, 335),
];
然后给小球添加一个碰撞状态,在碰撞时,给两个小球设置为不同的颜色:
class Circle {
constructor(context, x, y, r, vx, vy) {
// 其它代码
this.colliding = false;
}
draw() {
this.context.fillStyle = this.colliding
-
? “hsl(300, 100%, 70%)”
- “hsl(170, 100%, 50%)”;
// 其它代码
}
}
现在来判断小球之间是否发生了碰撞,这个条件很简单,判断两个小球圆心的距离是否小于两个小球的半径之和就可以了,如果小于等于则发生了碰撞,大于则没有发生碰撞。圆心的距离即计算两个坐标点的距离,可以用公式:
( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} (x1−x2)2+(y1−y2)2
x1、y1 和 x2、y2 分别两个小球的圆心坐标。在比较时,可以对半径和进行平方运算,进而省略对距离的开方运算,也就是可以用下方的公式进行比较:
( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 ≤ ( r 1 + r 2 ) 2 (x_1 - x_2)^2 + (y_1 - y_2)^2 \leq (r_1 + r_2)^2 (x1−x2)2+(y1−y2)2≤(r1+r2)2
r1 和 r2 为两球的半径。
在 Circle 类中,先添加一个isCircleCollided(other)
方法,接收另一个小球对象作为参数,返回比较结果:
isCircleCollided(other) {
let squareDistance =
(this.x - other.x) * (this.x - other.x) +
(this.y - other.y) * (this.y - other.y);
let squareRadius = (this.r + other.r) * (this.r + other.r);
return squareDistance <= squareRadius;
}
再添加 checkCollideWith(other)
方法,调用 isCircleCollided(other)
判断碰撞后,把两球的碰撞状态设置为 true:
checkCollideWith(other) {
if (this.isCircleCollided(other)) {
this.colliding = true;
other.colliding = true;
}
}
接着我们需要使用双循环两两比对小球是否发生了碰撞,由于小球数组存放在 Gameboard 对象中,我们给它添加一个 checkCollision()
方法来检测碰撞:
checkCollision() {
// 重置碰撞状态
this.circles.forEach((circle) => (circle.colliding = false));
for (let i = 0; i < this.circles.length; i++) {
for (let j = i + 1; j < this.circles.length; j++) {
this.circles[i].checkCollideWith(this.circles[j]);
}
}
}
因为小球在碰撞后就应立即弹开,所以我们一开始要把所有小球的碰撞状态设置为 false,之后在循环中,对每个小球进行检测。这里注意到内层循环是从 i + 1 开始的,这是因为在判断 1 球和 2 球是否碰撞后,就无须再判断 2 球 和 1 球了。
之后在 process()
方法中,执行检测,注意检测应该发生在使用 for 循环更新小球位置的后边才准确:
for (let i = 0; i < this.circles.length; i++) {
this.circles[i].update(seconds);
}
this.checkCollision();
现在,可以看到小球在碰撞时,会改变颜色了。
上边的代码在执行之后,小球都会穿过边界跑到外边去,那么我们先处理一下边界碰撞的问题。检测边界碰撞需要把四个面全部都处理到,根据圆心坐标和半径来判断是否和边界发生了碰撞。例如跟左边界发生碰撞时,圆心的 x 坐标是小于或等于半径长度的,而跟右边界发生碰撞时,圆心 x 坐标应该大于或等于画布最右侧坐标(即宽度值)减去半径的长度。上边界和下边界类似,只是使用圆心 y 坐标和画布的高度值。在水平方向上(即左右边界)发生碰撞时,小球的运动方向发生改变,只需要把垂直方向上的速度 vy 值取反即可,在垂直方向上碰撞则把 vx 取反。
现在看一下代码的实现,在 Gameboard 类中添加一个 checkEdgeCollision()
方法,根据上边描述的规则编写如下代码:
checkEdgeCollision() {
this.circles.forEach((circle) => {
// 左右墙壁碰撞
if (circle.x < circle.r) {
circle.vx = -circle.vx;
circle.x = circle.r;
} else if (circle.x > width - circle.r) {
circle.vx = -circle.vx;
circle.x = width - circle.r;
}
// 上下墙壁碰撞
if (circle.y < circle.r) {
circle.vy = -circle.vy;
circle.y = circle.r;
} else if (circle.y > height - circle.r) {
circle.vy = -circle.vy;
circle.y = height - circle.r;
}
});
}
在代码中,碰撞时,除了对速度进行取反操作之外,还把小球的坐标修改为紧临边界,防止超出。接下来在 process()
中添加对边界碰撞的检测:
this.checkEdgeCollision();
this.checkCollision();
这时候可以看到小球在碰到边界时,可以反弹了:
但是小球间的碰撞还没有处理,在处理之前,先复习一下向量的基本操作,数学好的同学可以直接跳过,只看相关的代码。
由于在碰撞时,需要对速度向量(或称为矢量)进行操作,向量是使用类似坐标的形式表示的,例如 < 3, 5 > (这里用 <> 表示向量),它有长度和方向,对于它的运算有一定的规则,本教程中需要用到向量的加法、减法、乘法、点乘和标准化操作。
向量相加只需要把两个向量的 x 坐标和 y 坐标相加即可,例如: < 3 , 5 > + < 1 , 2 > = < 4 , 7 > ❤️, 5> + <1, 2> = <4, 7> ❤️,5>+<1,2>=<4,7>
减法与加法类似,把 x 坐标和 y 坐标相减,例如: < 3 , 5 > − < 1 , 2 > = < 2 , 3 > ❤️, 5> - <1, 2> = <2, 3> ❤️,5>−<1,2>=<2,3>
乘法,这里指的是向量和标量的乘法,标量指的就是普通的数字,结果是把 x 和 y 分别和标量相乘,例如: 3 × < 3 , 5 > = < 9 , 15 > 3\times<3, 5> = <9, 15> 3×<3,5>=<9,15>。
点乘是两个向量相乘的一种方式,类似的还有叉乘,但是在本示例中用不到,点乘其实计算的是一个向量在另一个向量上的投影,它的计算方式为两个向量的 x 的积加上 y 的积,它返回的是一个标量,即第 1 个向量在第 2 个向量上投影的长度,例如: < 3 , 5 > ⋅ < 1 , 2 > = 3 × 1 + 5 × 2 = 13 ❤️, 5> \cdot <1, 2> = 3 \times 1 + 5 \times 2 = 13 ❤️,5>⋅<1,2>=3×1+5×2=13
标准化是除掉向量的长度,只剩下方向,这样的向量它的长度为 1,称为单位向量,标准化的过程是让 x 和 y 分别除以向量的长度,因为向量表示的是和原点(0, 0)的距离,所以可以直接使用 ( x 2 + y 2 ) \sqrt{(x^2 + y^2)} (x2+y2) 计算长度,例如 < 3, 4 > 标准化后的结果为: < 3 , 5 > ⋅ < 1 , 2 > = 3 × 1 + 5 × 2 = 13 ❤️, 5> \cdot <1, 2> = 3 \times 1 + 5 \times 2 = 13 ❤️,5>⋅<1,2>=3×1+5×2=13。
了解了向量的基本运算后,我们来创建一个 Vector 工具类,来方便我们进行向量的运算,它的代码就是实现了这些运算规则:
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
/**
-
向量加法
-
@param {Vector} v
*/
add(v) {
return new Vector(this.x + v.x, this.y + v.y);
}
/**
-
向量减法
-
@param {Vector} v
*/
substract(v) {
return new Vector(this.x - v.x, this.y - v.y);
}
/**
-
向量与标量乘法
-
@param {Vector} s
*/
multiply(s) {
return new Vector(this.x * s, this.y * s);
}
/**
-
向量与向量点乘(投影)
-
@param {Vector} v
*/
dot(v) {
return this.x * v.x + this.y * v.y;
}
/**
-
向量标准化(除去长度)
-
@param {number} distance
*/
normalize() {
let distance = Math.sqrt(this.x * this.x + this.y * this.y);
return new Vector(this.x / distance, this.y / distance);
}
}
代码中没有什么特殊的语法和操作,这里就不再赘述了,接下来我们看一下小球的碰撞问题。
碰撞处理最主要的部分就是计算碰撞后的速度和方向。通常最简单的碰撞问题是在同一个水平面上的两个物体的碰撞,称为一维碰撞,因为此时只需要计算同一方向上的速度,而我们现在的程序小球是在一个二维平面内运动的,小球之间发生正面相碰(即在同一运动方向)的概率很小,大部分是斜碰(在不同运动方向上擦肩相碰),需要同时计算水平和垂直方向上的速度和方向,这就属于是二维碰撞问题。不过,其实小球之间的碰撞,只有在连心线(两个圆心的连线)上有作用力,而在碰撞接触的切线方向上没有作用力,那么我们只需要知道连心线方向的速度变化就可以了,这样就转换成了一维碰撞。
计算碰撞后的速度时,遵守动量守恒定律和动能守恒定律,公式分别为:
动量守恒定律
m 1 v 1 + m 2 v 2 = m 1 v 1 ′ + m 2 v 2 ′ m_1v_1 + m_2v_2 = m_1v_1’ + m_2v_2’ m1v1+m2v2=m1v1′+m2v2′
动能守恒定律
1 2 m 1 v 1 2 + 1 2 m 2 v 2 2 = 1 2 m 1 v 1 ′ 2 + 1 2 m 2 v 2 ′ 2 \frac{1}{2}m_1v_12+\frac{1}{2}m_2v_22=\frac{1}{2}m_1v_1’2+\frac{1}{2}m_2v_2’2 21m1v12+21m2v22=21m1v1′2+21m2v2′2
m1、m2 分别为两小球的质量,v1 和 v2 为两小球碰撞前的速度向量,v1’ 和 v2’ 为碰撞后的速度向量。根据这两个公式可以推导出两小球碰撞后的速度公式:
v 1 ′ = v 1 ( m 1 − m 2 ) + 2 m 2 v 2 m 1 + m 2 v_1’=\frac{v_1(m_1-m_2)+2m_2v_2}{m_1+m_2} v1′=m1+m2v1(m1−m2)+2m2v2
v 2 ′ = v 2 ( m 2 − m 1 ) + 2 m 1 v 1 m 1 + m 2 v_2’=\frac{v_2(m_2-m_1)+2m_1v_1}{m_1+m_2} v2′=m1+m2v2(m2−m1)+2m1v1
如果不考虑小球的质量,或质量相同,其实就是两小球速度互换,即:
v 1 ′ = v 2 v_1’=v_2 v1′=v2
v 2 ′ = v 1 v_2’=v_1 v2′=v1
这里我们给小球加上质量,然后套用公式来计算小球碰撞后速度,先在 Circle 类中给小球加上质量 mass 属性:
class Circle {
constructor(context, x, y, r, vx, vy, mass = 1) {
// 其它代码
this.mass = mass;
}
}
然后在 Gameboard 类的初始化小球处,给每个小球添加质量:
this.circles = [
new Circle(ctx, 30, 50, 30, -100, 390, 30),
new Circle(ctx, 60, 180, 20, 180, -275, 20),
new Circle(ctx, 120, 100, 60, 120, 262, 100),
new Circle(ctx, 150, 180, 10, -130, 138, 10),
new Circle(ctx, 190, 210, 10, 138, -280, 10),
new Circle(ctx, 220, 240, 10, 142, 350, 10),
new Circle(ctx, 100, 260, 10, 135, -460, 10),
new Circle(ctx, 120, 285, 10, -165, 370, 10),
new Circle(ctx, 140, 290, 10, 125, 230, 10),
new Circle(ctx, 160, 380, 10, -175, -180, 10),
new Circle(ctx, 180, 310, 10, 115, 440, 10),
new Circle(ctx, 100, 310, 10, -195, -325, 10),
new Circle(ctx, 60, 150, 10, -138, 420, 10),
new Circle(ctx, 70, 430, 45, 135, -230, 45),
new Circle(ctx, 250, 290, 40, -140, 335, 40),
];
在 Circle 类中加上 changeVelocityAndDirection(other)
方法来计算碰撞后的速度,它接收另一个小球对象作为参数,同时计算这两个小球碰撞厚的速度和方向,这个是整个引擎的核心,我们一点一点的来看它是如何实现的。首先把两个小球的速度使用 Vector 向量来表示:
changeVelocityAndDirection(other) {
// 创建两小球的速度向量
let velocity1 = new Vector(this.vx, this.vy);
let velocity2 = new Vector(other.vx, other.vy);
}
因为我们本身就已经使用 vx 和 vy 来表示水平和垂直方向上的速度向量了,所以直接把它们传给 Vector 的构造函数就可以了。velocity1
和 velocity2
分别代表当前小球和碰撞小球的速度向量。
接下来获取连心线方向的向量,也就是两个圆心坐标的差:
let vNorm = new Vector(this.x - other.x, this.y - other.y);
接下来获取连心线方向的单位向量和切线方向上的单位向量,这些单位向量代表的是连心线和切线的方向:
let unitVNorm = vNorm.normalize();
let unitVTan = new Vector(-unitVNorm.y, unitVNorm.x);
unitVNorm
是连心线方向单位向量,unitVTan
是切线方向单位向量,切线方向其实就是把连心线向量的 x、y 坐标互换,并把 y 坐标取反。根据这两个单位向量,使用点乘计算小球速度在这两个方向上的投影:
let v1n = velocity1.dot(unitVNorm);
let v1t = velocity1.dot(unitVTan);
let v2n = velocity2.dot(unitVNorm);
let v2t = velocity2.dot(unitVTan);
计算结果是一个标量,也就是没有方向的速度值。v1n
和 v1t
表示当前小球在连心线和切线方向的速度值,v2n
和 v2t
则表示的是碰撞小球 的速度值。在计算出两小球的速度值之后,我们就有了碰撞后的速度公式所需要的变量值了,直接用代码把公式套用进去:
let v1nAfter = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass);
let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass);
v1nAfter
和 v2nAfter
分别是两小球碰撞后的速度,现在可以先判断一下,如果 v1nAfter
小于 v2nAfter
,那么第 1 个小球和第 2 个小球会越来越远,此时不用处理碰撞:
if (v1nAfter < v2nAfter) {
return;
}
然后再给碰撞后的速度加上方向,计算在连心线方向和切线方向上的速度,只需要让速度标量跟连心线单位向量和切线单位向量相乘:
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
- (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass);
let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass);
v1nAfter
和 v2nAfter
分别是两小球碰撞后的速度,现在可以先判断一下,如果 v1nAfter
小于 v2nAfter
,那么第 1 个小球和第 2 个小球会越来越远,此时不用处理碰撞:
if (v1nAfter < v2nAfter) {
return;
}
然后再给碰撞后的速度加上方向,计算在连心线方向和切线方向上的速度,只需要让速度标量跟连心线单位向量和切线单位向量相乘:
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-utCIpdz0-1715465952124)]
[外链图片转存中…(img-brWVP0t4-1715465952124)]
[外链图片转存中…(img-7TfDf9lo-1715465952125)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!