第五课:从小想法到大项目——编程综合实践
闪电编程,助你闪电入门!⚡️⚡️⚡️
本节课的大纲:
- 巩固基础:复习JavaScript核心语法和p5.js基本图形绘制
- 掌握新知:理解JavaScript类和对象的概念与应用
- 流程实践:学习从问题分析到代码实现的完整开发流程
- 项目实战:完成一个完整的p5.js交互游戏项目
本节课学完后能收获什么:
- 一个想法是如何变成代码项目的全流程
- 如何找出程序中的bug
- 编程中的高阶概念掌握:类和对象
- 通过大项目实践,明确工程项目的代码结构
课前准备:
- 本地Trae编程环境
- 一双敲代码的灵巧小手!
前言
在之前的课程里,我们一起探索了JavaScript、C++的基本语法,并打开了p5.js和Arduino两个神奇的创意工具箱。
但从绘制一个圆,到完成一个完整的交互艺术作品,这中间还有一段路要走。我们常常会遇到这样的困惑:
- 我有一个很酷的想法,但不知道如何把它变成一行行代码。
- 我的代码运行不起来,满屏的红色错误,我该怎么办?
- 业界大牛写的工程代码好复杂,我怎么连代码结构结构都看不懂?
今天这堂课,就是为了解决这些问题而设计的。我们将通过一个非常有趣的大项目,手把手地带领大家走完从“想法”到“作品”的全过程。
我们将一起学习如何规划代码、如何调试错误、如何规划工程代码结构,并最终完成一个p5.js的交互游戏项目。
第一部分:打牢基础——编程核心概念回顾
1.1 JavaScript核心语法复习
变量与数据类型
在JavaScript中,变量是存储数据的容器。我们可以使用let、const或var来声明变量,其中let是最常用的方式。
// 变量声明与赋值
let playerName = "艺术家"; // 字符串类型
let score = 0; // 数字类型
let isPlaying = true; // 布尔类型
let colors = ["red", "green", "blue"]; // 数组类型
// 变量更新
score = score + 10; // 现在score的值是10
练习1:创建一个描述你自己的变量集合,包括姓名、年龄、喜欢的颜色数组等。
数组操作
数组是存储多个值的有序列表,p5.js中经常用数组来管理游戏对象。
// 创建数组
let shapes = ["circle", "square", "triangle"];
// 访问数组元素
console.log(shapes[0]); // 输出: "circle"
// 添加元素
shapes.push("hexagon"); // 现在数组有4个元素
// 遍历数组
for (let i = 0; i < shapes.length; i++) {
console.log(shapes[i]);
}
// 或者使用更现代的forEach方法
shapes.forEach(shape => {
console.log(shape);
});
练习2:创建一个包含5种颜色的数组,然后使用循环将每个颜色打印到控制台。
条件判断
条件判断让程序能够根据不同情况执行不同代码。
let age = 20;
// if-else语句
if (age >= 18) {
console.log("成年人");
} else {
console.log("未成年人");
}
// 多重条件
let score = 85;
if (score >= 90) {
console.log("优秀");
} else if (score >= 80) {
console.log("良好");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
// 逻辑运算符
let isStudent = true;
let hasDiscount = false;
if (isStudent && hasDiscount) {
console.log("可以享受学生折扣");
}
练习3:编写一个程序,根据输入的数字判断它是正数、负数还是零。
循环结构
循环用于重复执行代码,是编程中最强大的工具之一。
// for循环 - 适合已知循环次数
for (let i = 0; i < 5; i++) {
console.log("这是第" + (i + 1) + "次循环");
}
// while循环 - 适合条件控制
let count = 0;
while (count < 3) {
console.log("count的值是: " + count);
count++;
}
练习4:使用循环计算1到100之间所有偶数的和。
函数定义与调用
函数是可重复使用的代码块,让程序更加模块化。
// 函数定义
function greet(name) {
return "你好, " + name + "!";
}
// 函数调用
let message = greet("小明");
console.log(message); // 输出: 你好, 小明!
// 带默认参数的函数
function createCircle(x = 50, y = 50, radius = 25) {
return {x: x, y: y, radius: radius};
}
let myCircle = createCircle(); // 使用默认值
let anotherCircle = createCircle(100, 100, 50); // 自定义值
练习5:创建一个函数,接收两个数字作为参数,返回它们的和、差、积、商。
1.2 p5.js基础图形与颜色
p5.js程序结构
p5.js程序有两个基本函数:setup()和draw()。
function setup() {
// 初始化代码,只运行一次
createCanvas(400, 400); // 创建400x400的画布
background(220); // 设置背景色为浅灰色
}
function draw() {
// 每秒运行60次的循环代码
// 这里放置需要持续更新的内容
}
基本形状绘制
p5.js提供了丰富的绘图函数。
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
// 绘制矩形 (x, y, 宽度, 高度)
rect(50, 50, 100, 80);
// 绘制圆形 (x, y, 宽度, 高度)
ellipse(200, 200, 80, 80);
// 绘制三角形 (三个顶点的坐标)
triangle(300, 100, 350, 200, 250, 200);
// 绘制线条 (起点x, 起点y, 终点x, 终点y)
line(50, 300, 350, 300);
}
颜色控制
p5.js提供了多种设置颜色的方法。
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
// 设置填充颜色 (RGB)
fill(255, 0, 0); // 红色
rect(50, 50, 80, 80);
// 设置描边颜色和粗细
stroke(0, 0, 255); // 蓝色描边
strokeWeight(5); // 描边粗细为5像素
fill(0, 255, 0); // 绿色填充
ellipse(200, 200, 100, 100);
// 使用颜色名称
fill("purple");
noStroke(); // 无描边
triangle(300, 100, 350, 200, 250, 200);
// 透明度设置
fill(255, 0, 0, 100); // 半透明红色
rect(100, 300, 200, 80);
}
第二部分:用蓝图思维创作 —— 类与对象
目标:引入面向对象编程(OOP)的核心思想。
2.1 为什么需要 “面向对象”?
我们先看两个现实问题:
- 如何用代码描述 “你的手机”?(品牌、价格、能打电话、能上网)
- 如何快速描述 “全班 30 个同学”?(每人都有姓名、年龄,都能学习、吃饭)
如果只用变量 / 函数写,会变成这样:
// 描述1个手机
let phoneBrand = "华为";
let phonePrice = 4999;
function phoneCall() { console.log("打电话"); }
function phoneSurf() { console.log("上网"); }
// 描述2个学生
let stu1Name = "小明";
let stu1Age = 18;
function stu1Study() { console.log("小明学习"); }
let stu2Name = "小红";
let stu2Age = 17;
function stu2Study() { console.log("小红学习"); }
//(30个会写疯!)
问题:重复代码多、数据和功能分散(姓名和对应的学习方法没绑定)。
解决方案:
面向对象编程(OOP)——
把 “数据 + 功能” 打包成 “对象”,用 “类” 批量创建对象。
2.2 认识 “对象”—— 现实事物的代码映射
2.2.1 什么是对象?
现实中:任何具体的事物都是对象(你的手机、同桌、课本),它有两个核心:
- 「属性」:事物的特征(手机的品牌、价格;学生的姓名、年龄)
- 「方法」:事物的行为(手机能打电话;学生能学习)
JS 中:
对象是 “键值对的集合” ,键是属性 / 方法名,值是属性值 / 函数。
2.2.2 用 “对象字面量” 创建第一个对象
最基础的创建方式({} 包裹),直接对应现实事物:
// 1. 描述“你的手机”对象
const myPhone = {
// 属性:特征(键值对,值是基本数据类型)
brand: "苹果",
price: 5999,
color: "黑色",
isNew: true,
// 方法:行为(键值对,值是函数)
call: function (name) {
console.log(`用${this.brand}给${name}打电话`); // this指向当前对象(myPhone)
},
surf: function () {
console.log(`用${this.color}的手机刷抖音`);
}
};
// 使用对象的属性和方法
console.log(myPhone.brand); // 取属性:苹果
myPhone.call("妈妈"); // 调用方法:用苹果给妈妈打电话
myPhone.surf(); // 调用方法:用黑色的手机刷抖音
小练习:创建 “学生” 对象
试着用对象字面量描述 “你自己”,包含:
- 属性:name(姓名)、age(年龄)、grade(年级)
- 方法:study(输出 “XX 在学习 JS”)、eat(输出 “XX 在吃午饭”)
const myself = {
name: "",
age: "",
grade: "",
study: function() {
// 补充代码
},
eat: function() {
// 补充代码
}
};
myself.study(); // 测试:例如输出“张三在学习JS”
2.3 遇见 “类”—— 对象的 “模板工厂”
2.3.1 痛点:多个相似对象的重复劳动
如果要创建 3 个学生对象,用字面量会重复写name/age/study:
// 重复代码太多!
const stu1 = { name: "小明", age: 18, study() { ... } };
const stu2 = { name: "小红", age: 17, study() { ... } };
const stu3 = { name: "小李", age: 18, study() { ... } };
核心需求:需要一个 “模板”,规定学生必须有name/age属性和study方法,用模板批量造对象。
2.3.2 什么是 “类”?
JS 中的class(类)就是对象的模板,它定义了:
- 该类的对象 “必须有哪些
属性”
- 该类的对象 “必须有哪些
方法”
类比:
“手机模板”(类)规定了手机要有品牌、价格、打电话功能;用这个模板造出来的 “华为手机”“苹果手机” 就是 “对象”(实例)。
2.3.3 用class定义第一个类
语法:
class 类名 { constructor(属性) { ... } 方法() { ... } }
// 1. 定义“学生类”(模板)
class Student {
// 构造函数:初始化对象的属性(必写,创建对象时自动执行)
constructor(name, age, grade) {
this.name = name; // this指向“即将创建的对象”
this.age = age;
this.grade = grade;
}
// 方法:类的对象能直接调用(不用写function关键字)
study(subject) {
console.log(`${this.name}(${this.grade})在学习${subject}`);
}
//方法:吃饭
eat(food) {
console.log(`${this.name}在吃${food}`);
}
}
2.3.4 用new创建类的 “实例”(对象)
- 创建对象:
const 对象名 = new 类名(参数)
根据模板造对象,参数传给constructor函数
- 使用对象:
对象名.属性名 对象名.方法名()
// 2. 用Student类创建3个学生对象
const stu1 = new Student("小明", 18, "高三1班");
const stu2 = new Student("小红", 17, "高三2班");
const stu3 = new Student("小李", 18, "高三1班");
// 3. 使用对象的属性和方法
console.log(stu1.age); // 18(取属性)
stu1.study("面向对象"); // 小明(高三1班)在学习面向对象(调用方法)
stu2.eat("汉堡"); // 小红在吃汉堡
对比优势:
30 个学生也只需写 30 行new Student(...),无需重复定义方法!
- 类(Class): 模板,定义属性(如名字、身高)和方法(如走路、说话)。
- 对象(Object): 根据模板创建的具体实例。
2.4 艺术应用示例:创建“画笔”类
// 定义“画笔”类
class Brush {
// 构造函数:设置画笔初始属性
constructor(x, y, color) {
this.x = x; // 位置x
this.y = y; // 位置y
this.color = color; // 颜色
this.size = 20; // 大小
}
// 方法:画圆
drawCircle() {
fill(this.color);
noStroke();
circle(this.x, this.y, this.size);
}
// 方法:移动
move(newX, newY) {
this.x = newX;
this.y = newY;
}
}
// 创建红色画笔
let redBrush = new Brush(100, 100, "red");
redBrush.drawCircle(); // 在(100,100)画红圆
// 移动画笔并重新画
redBrush.move(200, 200);
redBrush.drawCircle(); // 在(200,200)画红圆
2.5 练习:创建 “手机类”
定义Phone类,要求:
- 属性:brand(品牌)、price(价格)、storage(存储)
- 方法:
-
- call (name):输出 “用 XX 手机给 XX 打电话”
-
- showInfo ():输出 “品牌:XX,价格:XX,存储:XX”
// 定义Phone类
class Phone {
// 补充构造函数
constructor(brand, price, storage) {
// 代码
}
// 补充call方法
call(name) {
// 代码
console.log()
}
// 补充showInfo方法
showInfo() {
// 代码
}
}
// 测试:创建华为手机对象并使用
const huawei = new Phone("华为", 4999, "256G");
huawei.call("爸爸"); // 预期:用华为手机给爸爸打电话
huawei.showInfo(); // 预期:品牌:华为,价格:4999,存储:256G
第三部分:从想法到代码——设计师编程工作流拆解
学习完编程基础后,你肯定还会有以下困惑:
我有一个想法,但是怎么变成代码?
AI好笨啊,生成的代码为什么和我想要的差别这么大!
3.1 编码流程拆解
当我们面对一个编程任务时,可以按照以下步骤进行:
- 理解需求:明确我们要实现什么功能
- 分解任务:将大问题拆分成小问题
- 伪代码:用自然语言描述解决方案
- 编写代码:将伪代码转换为实际代码(可让AI做)
- 测试调试:检查代码是否按预期工作
- 优化改进:让代码更好、更高效
一个普遍的误解是,编程就是坐下来敲代码。但实际上,最厉害的程序员在写下第一行真正的代码之前,用一种叫做“伪代码 ”的工具来规划整体思路。
伪代码不是任何一种具体的编程语言,它没有严格的语法。它就是用我们自己的、最自然的大白话,把程序的逻辑步骤清晰地写下来。
对于初学者来说,最大的障碍往往不是忘记了某个函数的拼写,而是面对一个宏大的创意目标时,大脑一片空白。伪代码强制我们暂时忘记语法细节,专注于思考问题的核心逻辑。
3.2 创意的细节——伪代码实践
一个创意,最开始可能只有一个目标,一句话。
我们需要对这句话的细节进行丰富,并用伪代码描述出来。
伪代码也是自然语言,但是它说明了程序的结构 和流程。
创意目标:
“我想做一个交互效果:画面上有一片花田,当鼠标按住时,花朵会向上生长。”
伪代码规划过程:
-
第一步:识别场景中的“事物”或“角色”。
- 很明显,我们需要“花”(
Flower)
- 很明显,我们需要“花”(
-
第二步:为这个“事物”设计蓝图 (类 Class)。
-
一朵“花”有什么属性 (Properties) ?
- 它需要有
x和y坐标来确定位置。 - 它需要有
stemHeight(花茎高度)。 - 它需要有
petalSize(花瓣大小)。 - 它需要有自己的
color。
- 它需要有
-
一朵“花”能做什么 方法(Methods) ?
- 它应该能
grow()(生长),也就是让自己的花茎变长。 - 它应该能
display()(显示),也就是把自己画在屏幕上。
- 它应该能
-
创建 Flower 类:
constructor() 属性:
- x: 花朵的x坐标
- y: 花朵的y坐标
- stemHeight: 花茎高度
- petalSize: 花瓣大小
- color: 花朵颜色
方法:
grow():
增加花茎高度
增加花瓣大小
display():
绘制花茎
绘制花朵
3. 第三步:规划程序的整体结构 (setup 和 draw)。
* **在 `setup()` 里要做什么? (初始化)**
setup():
创建画布
初始化 garden 为空数组
循环50次:
创建新的 Flower 对象
给花朵随机的x、y位置
添加到 garden 数组
- 在
draw()里要做什么? (每一帧的循环)
draw():
绘制背景
遍历 garden 数组中的每朵花:
如果鼠标被按下:
调用当前花朵的 grow() 方法
调用当前花朵的 display() 方法
4. 伪代码翻译为编程语言
将上述伪代码,翻译成javascript代码,即可实现自己的创意。
- 如果定位自己是一名程序员 : 手动实现js代码
- 如果你认为自己是设计师or产品经理: 把伪代码交给AI,让它实现js代码。
我是一名设计师,我现在要使用p5.js实现【xxx功能】(画面上有一片花田,当鼠标按住时,花朵会向上生长),根据我给出的伪代码,生成完整可运行的javascript代码。
要求:注释明确,程序结构清晰。
伪代码如下: 【伪代码】
翻译后的js代码:
// 花朵类
class Flower {
constructor(x, y) {
// 花的属性
this.x = x;
this.y = y;
this.stemHeight = 10;
this.petalSize = 5;
this.color = color(random(255), random(255), random(255));
}
// 生长方法
grow() {
this.stemHeight += 1;
this.petalSize += 0.2;
}
// 显示方法
display() {
// 绘制花茎
stroke(0, 255, 0); // 绿色
line(this.x, this.y, this.x, this.y - this.stemHeight);
// 绘制花朵
fill(this.color);
noStroke();
ellipse(this.x, this.y - this.stemHeight, this.petalSize * 2);
}
}
let garden = []; // 存储花朵的数组
function setup() {
createCanvas(600, 400);
// 创建50朵花
for (let i = 0; i < 50; i++) {
let x = random(width);
let y = height - 50; // 固定在底部附近
garden.push(new Flower(x, y));
}
}
function draw() {
// 绘制背景
background(135, 206, 235); // 天空蓝
fill(50, 205, 50); // 草地绿
rect(0, height - 50, width, 50);
// 处理每一朵花
for (let flower of garden) {
if (mouseIsPressed) {
flower.grow();
}
flower.display();
}
}
3.3 程序有问题怎么办?——p5.js代码调试
调试是编程中的重要技能: 代码放上去了,但是并没有按我们想要的效果执行,那么我们该怎么找出问题呢?
这就是我们常说的bug, 找出这些问题的过程就是调试,俗称debug
在p5.js中进行debug,存在下面这些实用技巧:
1. 使用console.log()
最简单的调试方法,将变量值输出到浏览器控制台(按F12打开)。
也可以使用console.warn("") 输出警告⚠信息
let x = 0;
function setup() {
createCanvas(400, 400); // 创建一个400x400像素的画布
}
function draw() {
background(220);
text("右键单击网页空白处,点击检查,查看控制台输出!",100,100)
x += 1;
console.log("x的值是: " + x); // 在控制台查看x的变化
ellipse(x, height/2, 50);
console.warn("圆又移动了一点!");
}
2. 使用print()函数
p5.js特有的打印方法,输出到编辑器控制台。
// 将console.log()进行替换
print("x的值是: " + x);
// 在控制台查看x的变化
3. 使用debugger语句
在代码中设置断点,程序执行到此处会暂停。
1.加入下面的代码到js文件中
2.右键点开控制台,然后点击画布,看看发生了什么
3.右下角的监视,添加并查看所有变量在停顿时的值。
function mousePressed() {
debugger; // 程序会在这里暂停
console.log("鼠标被点击了");
}
3.4 【练习:大家来找茬】
让我们来看一段有bug的代码。
这段代码的本意是:当鼠标在画布左半边时,背景为蓝色;在右半边时,背景为红色。但现在它好像不工作了。
有bug的代码:
function setup() {
createCanvas(400, 400);
}
function draw() {
// 我们想根据mouseX的位置改变背景色
let halfWidth = width / 2;
if (mouseX > halfwidth) { // 注意这里的拼写
background('red');
} else {
background('blue');
}
// 打印一些信息来帮助我们调试
console.log("当前鼠标X坐标: " + mouseX);
console.log("画布一半宽度是: " + halfWidth);
}
调试步骤:
- 将这段代码复制到编辑器中运行。
- 在浏览器中,右键点击页面,选择“检查”(Inspect),打开开发者工具,然后切换到“控制台”(Console)标签页。
- 你会看到一个红色的错误信息:
Uncaught ReferenceError: halfwidth is not defined。 - 这个错误告诉我们,
halfwidth这个变量没有被定义。我们回头检查代码,发现定义变量时用的是驼峰命名法halfWidth,而在if语句中,我们不小心写成了全小写的halfwidth。JavaScript是大小写敏感的! - 修正代码: 将
if (mouseX > halfwidth)改为if (mouseX > halfWidth)。
总结:
打印变量 和开发者控制台是debug的好伙伴。
当你感觉代码“不对劲”时,就用 console.log() 或者 print() 或者 debugger 把你怀疑的变量打印出来看看,真相往往就藏在这些数值里。
第四部分:从0到1完成一个综合编程项目——太空射击游戏
项目概述
完成一个"飞机大战"游戏,玩家控制飞船躲避或击落从上方落下的敌人。
这是一个将今天所学全部知识融会贯通的综合性项目。一步一步,从一个空白的画布开始,构建一个完整、可玩的互动游戏。
每一步都会在前一步的基础上增加新的功能层,让大家清晰地看到一个复杂的项目是如何逐步成形的。
在做的过程中,注意区分:
- 小作坊式代码开发模式
- 业界工程项目标准代码模式
游戏关键元素
- 玩家:控制底部的飞船,可以左右移动
- 控制:键盘左右控制飞船移动,按空格发射子弹
- 敌人:从顶部随机位置出现,向下移动
- 目标:尽可能长时间生存,击落敌人获得分数
- 结束条件:被敌人撞击
第0步:准备工作——创建最基础的p5.js项目
在你的index.HTML文件中,输入下面的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 引入 p5.js 核心库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.js"></script>
<meta charset="utf-8" />
</head>
<body>
<main>
</main>
<!-- 引入你自己的 sketch 文件 -->
<script src="sketch.js"></script>
</body>
</html>
在你的 sketch.js 文件中,输入以下最基础的p5.js模板代码:
// sketch.js
function setup() {
createCanvas(400, 600);
}
function draw() {
background(220);
}
代码说明:
setup()函数只运行一次,用于初始化画布。draw()函数每秒运行约60次,用于绘制动画。
第1步:绘制一个静态的玩家飞船
我们的第一个目标是在画布底部中央绘制一个代表玩家飞船的三角形。
变更位置:在 draw() 函数中添加绘图代码。
修改后的代码:
function setup() {
createCanvas(400, 600);
}
function draw() {
// 绘制带拖尾效果的深色背景
background(10, 10, 30, 20);
// 绘制玩家飞船(一个三角形)
fill('cyan');
noStroke();
beginShape();
vertex(200, 540); // 顶部顶点 (x=200, y=600-60)
vertex(180, 580); // 左下顶点
vertex(220, 580); // 右下顶点
endShape(CLOSE);
}
代码说明:
- 我们硬编码了飞船的位置 (200, 540),其中
200是画布宽度的一半 (400/2),540是 600 - 60(画布高度减去一个偏移量)。 beginShape()和endShape(CLOSE)用于绘制自定义形状, 代表所有vertex点的连线。- 问题:这个飞船是静态的,无法移动。而且所有数据(位置、颜色)都写死在
draw()里,难以管理和修改。
第2步:用变量管理飞船状态
为了解决硬编码的问题,我们将飞船的位置和尺寸提取为变量。
变更位置:在文件顶部声明全局变量,并在 draw() 中使用它们。
修改后的代码:
// === 新增:全局变量 ===
let shipX = 200;
let shipY = 540;
let shipWidth = 40;
let shipHeight = 40;
function setup() {
createCanvas(400, 600);
}
function draw() {
// 绘制带拖尾效果的深色背景
background(10, 10, 30, 20);
// 使用变量绘制飞船
fill('cyan');
noStroke();
let halfW = shipWidth / 2;
let halfH = shipHeight / 2;
beginShape();
vertex(shipX, shipY - halfH);
vertex(shipX - halfW, shipY + halfH);
vertex(shipX + halfW, shipY + halfH);
endShape(CLOSE);
}
代码说明:
-
现在,我们可以通过修改
shipX和shipY的值来改变飞船的位置。 -
问题:这比硬编码好多了,但仍然不够优雅。如果游戏里有多个飞船,我们需要为每个飞船创建一套变量,非常麻烦。
第3步:引入类的概念——创建Player类
现在,我们正式引入类(Class)。
类可以用来创建多个具有相同属性和行为的对象。我们创建一个玩家类,代表玩家所操纵的飞船。
变更位置:在文件顶部定义 Player 类,并用其实例替换全局变量。
修改后的代码:
// === 新增:Player 类定义 ===
class Player {
constructor() {
// 在构造函数中初始化属性
this.x = width / 2;
this.y = height - 60;
this.width = 40;
this.height = 40;
this.color = 'cyan';
}
}
// === 修改:用 Player 实例替换全局变量 ===
let myShip; // 声明一个变量来存放 Player 实例
function setup() {
createCanvas(400, 600);
// 创建 Player 的一个实例
myShip = new Player();
}
function draw() {
// 绘制带拖尾效果的深色背景
background(10, 10, 30, 20);
// 使用 myShip 实例的属性来绘制
fill(myShip.color);
noStroke();
let halfW = myShip.width / 2;
let halfH = myShip.height / 2;
beginShape();
vertex(myShip.x, myShip.y - halfH);
vertex(myShip.x - halfW, myShip.y + halfH);
vertex(myShip.x + halfW, myShip.y + halfH);
endShape(CLOSE);
}
代码说明:
class Player { ... }定义了一个名为Player的类。constructor()是特殊的方法,当使用new Player()创建实例时会自动调用。this关键字指向当前正在创建的对象实例。this.x表示这个实例的x属性。myShip = new Player();创建了一个Player的实例,并将其赋值给myShip变量。- 优势:现在,飞船的所有状态(位置、尺寸、颜色)都被封装在
myShip这一个对象里,代码更整洁、更易扩展。
第4步:将绘图逻辑封装到类的方法中
目前,绘制飞船的代码还在 draw() 函数里。我们可以将这部分逻辑也封装到 Player 类中,让类自己负责“如何显示自己”:绘制飞船的代码——放到玩家飞船类里。
变更位置:在 Player 类中添加 show() 方法,并在 draw() 中调用它。
修改后的代码:
class Player {
constructor() {
this.x = width / 2;
this.y = height - 60;
this.width = 40;
this.height = 40;
this.color = 'cyan';
}
// === 新增:show 方法 ===
show() {
fill(this.color);
noStroke();
let halfW = this.width / 2;
let halfH = this.height / 2;
beginShape();
vertex(this.x, this.y - halfH);
vertex(this.x - halfW, this.y + halfH);
vertex(this.x + halfW, this.y + halfH);
endShape(CLOSE);
}
}
let myShip;
function setup() {
createCanvas(400, 600);
myShip = new Player();
}
function draw() {
// 绘制带拖尾效果的深色背景
background(10, 10, 30, 20);
// === 修改:直接调用 myShip 的 show 方法 ===
myShip.show();
}
代码说明:
show()是Player类的一个方法,它定义了该类实例的行为。- 在
show()内部,this指向调用该方法的对象(即myShip),所以this.x就是myShip.x。 - 优势:现在,
draw()函数变得非常简洁。如果将来要修改飞船的外观,我们只需要修改Player类内部的show()方法,而不需要改动主程序逻辑。这就是封装的力量。
第5步:实现飞船的键盘控制
现在让飞船动起来, 通过键盘的左右箭头键来控制飞船移动。
变更位置:
- 在
Player类中添加move()方法。 - 在文件顶部添加全局标志变量。
- 添加
keyPressed()和keyReleased()函数。 - 在
draw()中根据标志变量调用move()。
修改后的代码:
class Player {
constructor() {
this.x = width / 2;
this.y = height - 60;
this.width = 40;
this.height = 40;
this.color = 'cyan';
this.speed = 5; // 新增:移动速度
}
show() {
fill(this.color);
noStroke();
let halfW = this.width / 2;
let halfH = this.height / 2;
beginShape();
vertex(this.x, this.y - halfH);
vertex(this.x - halfW, this.y + halfH);
vertex(this.x + halfW, this.y + halfH);
endShape(CLOSE);
}
// === 新增:move 方法 ===
move(direction) {
if (direction === 'left') {
this.x -= this.speed;
} else if (direction === 'right') {
this.x += this.speed;
}
// 限制飞船不能移出画布
this.x = constrain(this.x, this.width/2, width - this.width/2);
}
}
let myShip;
// === 新增:键盘状态标志 ===
let movingLeft = false;
let movingRight = false;
function setup() {
createCanvas(400, 600);
myShip = new Player();
}
// === 新增:键盘事件处理函数 ===
function keyPressed() {
if (keyCode === LEFT_ARROW) {
movingLeft = true;
}
if (keyCode === RIGHT_ARROW) {
movingRight = true;
}
}
function keyReleased() {
if (keyCode === LEFT_ARROW) {
movingLeft = false;
}
if (keyCode === RIGHT_ARROW) {
movingRight = false;
}
}
function draw() {
background(10, 10, 30, 20);
// === 修改:根据按键状态移动飞船 ===
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
}
代码说明:
constrain(value, min, max)是p5.js的函数,用于将值限制在指定范围内,防止飞船飞出画布。keyPressed()和keyReleased()是p5.js的特殊函数,当键盘按键被按下或松开时会自动调用。- 我们使用
movingLeft和movingRight两个布尔变量来追踪按键状态,这样可以在draw()循环中持续移动飞船,而不是只在按键瞬间移动一次。 - 核心思想:我们将“移动”的逻辑也封装到了
Player类中,主程序只需要告诉飞船“向左”或“向右”移动即可,无需关心具体如何计算坐标。
第6步:创建Enemy类并生成单个敌人
现在飞船可以移动了。接下来,让我们添加一个敌人(小行星)。
变更位置:
- 定义
Enemy类。 - 创建一个
enemy变量, - 在
setup()中使用new()初始化敌人。 - 在
draw()中显示敌人。
修改后的代码:
// ... Player 类代码保持不变 ...
// === 新增:Enemy 类 ===
class Enemy {
constructor(x, y, size) {
this.x = x;
this.y = y;
this.size = size;
// 随机生成灰色调
this.color = color(random(100, 200), random(100, 200), random(100, 200));
// 敌人的移动速度
this.speed = random(1, 4);
}
show() {
fill(this.color);
noStroke();
rect(this.x, this.y, this.size, this.size);
}
move() {
this.y += this.speed; // 向下移动
}
}
let myShip;
let enemy; // 新增:单个敌人变量
let movingLeft = false;
let movingRight = false;
function setup() {
createCanvas(400, 600);
myShip = new Player();
// 新增,初始化一个敌人
enemy = new Enemy(200, 0, 30);
}
// ... keyPressed 和 keyReleased 保持不变 ...
function draw() {
bbackground(10, 10, 30, 20);
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
// === 新增:更新和显示敌人 ===
enemy.move();
enemy.show();
}
代码说明:
Enemy类的结构与Player类非常相似,这体现了面向对象编程的一致性。constructor(x, y, size)接收参数,使得我们可以灵活地创建不同位置和大小的敌人。- 现在,画布上会有一个灰色方块从顶部向下掉落。
第7步:用数组管理多个敌人
我们需要让敌人源源不断地出现, 使用数组来管理多个敌人。
变更位置:
- 将
enemy变量改为enemies数组。 - 在
setup()中初始化空数组。 - 在
draw()中使用循环遍历数组。 - 添加敌人生成逻辑。
修改后的代码:
// ... Player 和 Enemy 类保持不变 ...
let myShip;
// let enemy; // 删除单个敌人变量
let enemies = []; // === 修改:改为敌人数组 ===
let movingLeft = false;
let movingRight = false;
//===删除:setup()内初始化敌人语句
function setup() {
createCanvas(400, 600);
myShip = new Player();
// enemies = []; // 数组已在声明时初始化
}
// ... keyPressed 和 keyReleased 保持不变 ...
function draw() {
background(10, 10, 30, 20);
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
// === 修改:遍历 enemies 数组 ===
for (let i = 0; i < enemies.length; i++) {
enemies[i].move();
enemies[i].show();
}
// === 新增:周期性生成敌人 ===
if (frameCount % 60 === 0) { // 每60帧(约1秒)生成一个
let x = random(0, width);
let size = random(20, 40);
enemies.push(new Enemy(x, 0, size));
}
}
代码说明:
enemies = []创建了一个空数组。enemies.push(new Enemy(...))将新创建的敌人实例添加到数组末尾。for (let i = 0; i < enemies.length; i++)是遍历数组的标准方式,enemies[i]代表数组中的第i个敌人。frameCount是p5.js的全局变量,记录draw()被调用的次数。frameCount % 60 === 0实现了每秒生成一个敌人的效果。- 优势:现在,无论有多少个敌人,我们都可以用同一个循环来统一处理它们。这就是数组和循环结合的强大力量。
第8步:创建Bullet类并实现射击功能
射击需要武器🔫!创建 Bullet 类,并实现按下空格键射击的功能。
变更位置:
- 定义
Bullet类。 - 添加
bullets数组。 - 在
keyReleased()中添加射击逻辑。 - 在
draw()中更新和显示子弹。
修改后的代码:
// ... Player 和 Enemy 类保持不变 ...
// === 新增:Bullet 类 ===
class Bullet {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 4;
this.height = 12;
this.color = 'yellow';
this.speed = -8; // 负值表示向上移动
}
show() {
fill(this.color);
noStroke();
rect(this.x - this.width/2, this.y, this.width, this.height);
}
move() {
this.y += this.speed;
}
}
let myShip;
let enemies = [];
let bullets = []; // === 新增:子弹数组 ===
let movingLeft = false;
let movingRight = false;
function setup() {
createCanvas(400, 600);
myShip = new Player();
}
function keyPressed() {
if (keyCode === LEFT_ARROW) {
movingLeft = true;
}
if (keyCode === RIGHT_ARROW) {
movingRight = true;
}
}
function keyReleased() {
if (keyCode === LEFT_ARROW) {
movingLeft = false;
}
if (keyCode === RIGHT_ARROW) {
movingRight = false;
}
// === 新增:空格键射击 ===
if (keyCode === 32) { // 32 是空格键的代码
// 子弹从飞船顶部中心发射
let bulletX = myShip.x;
let bulletY = myShip.y - myShip.height/2;
bullets.push(new Bullet(bulletX, bulletY));
}
}
function draw() {
background(220);
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
// 更新和显示敌人
for (let i = 0; i < enemies.length; i++) {
enemies[i].move();
enemies[i].show();
}
// === 新增:更新和显示子弹 ===
for (let i = 0; i < bullets.length; i++) {
bullets[i].move();
bullets[i].show();
}
// 生成敌人
if (frameCount % 60 === 0) {
let x = random(0, width);
let size = random(20, 40);
enemies.push(new Enemy(x, 0, size));
}
}
代码说明:
Bullet类的结构再次与之前的类保持一致,体现了代码的可复用性。- 射击时,子弹的初始位置
(bulletX, bulletY)是根据飞船的当前位置计算得出的。 bullets数组的管理方式与enemies数组完全相同。
第9步:清理越界的子弹和敌人
随着时间推移,飞出画布的子弹和敌人会一直存在于数组中,浪费计算机内存(内存占用过多,电脑会卡死)。因此需要清理它们。
变更位置:
1.修改 draw() 中遍历 bullets 和 enemies 的循环,改为从后往前遍历(删除元素后,前面还没遍历的元素索引不变)
2.添加子弹和敌人飞出画布的越界检查,如果越界则从数组中删除。
修改后的代码:
// ... 所有类和变量声明保持不变 ...
function draw() {
background(10, 10, 30, 20);
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
// === 修改:从后往前遍历 enemies 数组 ===
for (let i = enemies.length - 1; i >= 0; i--) {
enemies[i].move();
enemies[i].show();
// 清理到达底部的敌人
if (enemies[i].y > height) {
enemies.splice(i, 1); // 从数组中移除
}
}
// === 修改:从后往前遍历 bullets 数组 ===
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].move();
bullets[i].show();
// 清理飞出顶部的子弹
if (bullets[i].y < 0) {
bullets.splice(i, 1);
}
}
// 生成敌人
if (frameCount % 60 === 0) {
let x = random(0, width);
let size = random(20, 40);
enemies.push(new Enemy(x, 0, size));
}
}
代码说明:
- 为什么从后往前遍历? 当我们使用
splice(i, 1)从数组中移除元素时,数组的长度会变短,后面的元素索引会前移。如果从前向后遍历,可能会跳过下一个元素。从后往前遍历则可以避免这个问题。 splice(i, 1)会从索引i开始,移除1个元素。
第10步:实现碰撞检测
我们需要检测子弹是否击中了敌人,以及敌人是否撞到了玩家。——游戏的核心:碰撞检测
变更位置:在 draw() 中添加碰撞检测逻辑。并添加score得分变量。
-
碰撞的判定: 如果两个物体中心点的距离小于它们“半径”之和,就认为发生了碰撞。
dist()函数用来检测距离。 -
消灭敌人——子弹和敌人的碰撞检测:两层循环,遍历敌人数组和子弹数组。
-
炸机——敌人和玩家飞船的碰撞检测:一层循环,遍历敌人数组。
-
碰撞发生时,我们同时从两个数组中移除对应的对象,并增加得分。
修改后的代码:
// ... 在文件顶部添加得分变量 ...
let score = 0;
// ... 所有类和变量声明保持不变 ...
function draw() {
background(220);
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
// 更新和清理敌人
for (let i = enemies.length - 1; i >= 0; i--) {
enemies[i].move();
enemies[i].show();
if (enemies[i].y > height) {
enemies.splice(i, 1);
}
}
// 更新和清理子弹
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].move();
bullets[i].show();
if (bullets[i].y < 0) {
bullets.splice(i, 1);
}
}
// === 新增:子弹与敌人的碰撞检测 ===
for (let i = bullets.length - 1; i >= 0; i--) {
for (let j = enemies.length - 1; j >= 0; j--) {
// 计算子弹和敌人中心点之间的距离
let d = dist(bullets[i].x, bullets[i].y, enemies[j].x, enemies[j].y);
// 简化的碰撞半径:取子弹和敌人宽度的一半之和
let hitRadius = (bullets[i].width + enemies[j].size) / 2;
if (d < hitRadius) {
// 碰撞发生!
bullets.splice(i, 1); // 移除子弹
enemies.splice(j, 1); // 移除敌人
score += 10; // 增加得分
break; // 一颗子弹只能击中一个敌人,跳出内层循环
}
}
}
// === 新增:玩家与敌人的碰撞检测 ===
for (let i = enemies.length - 1; i >= 0; i--) {
let d = dist(myShip.x, myShip.y, enemies[i].x, enemies[i].y);
let hitRadius = (myShip.width + enemies[i].size) / 2;
if (d < hitRadius) {
console.log("Game Over!");
// TODO: 实现游戏结束逻辑
}
}
// === 新增: 显示得分
fill(255);
textSize(16);
textAlign(LEFT);
text("Score: " + score, 20, 30);
// 生成敌人
if (frameCount % 60 === 0) {
let x = random(0, width);
let size = random(20, 40);
enemies.push(new Enemy(x, 0, size));
}
}
代码说明:
dist(x1, y1, x2, y2)是p5.js的函数,用于计算两点之间的距离。- 我们使用了一个简化的碰撞模型:如果两个物体中心点的距离小于它们“半径”之和,就认为发生了碰撞。
- 双重循环:外层循环遍历所有子弹,内层循环遍历所有敌人,确保每一对组合都被检查到。
- 当碰撞发生时,我们同时从两个数组中移除对应的对象,并增加得分。
第11步:添加游戏结束逻辑
最后一步,完善游戏结束的体验。
变更位置:
- 添加
gameRunning状态变量。 - 创建
gameOver()函数。 - 在
draw()开头检查游戏状态。 - 在碰撞检测中调用
gameOver()。
// 用于追踪游戏是否正在进行
let gameRunning = true;
// 游戏结束函数
// ==============================
function gameOver() {
gameRunning = false;
// 显示游戏结束信息
fill(255, 0, 0);
textSize(32);
textAlign(CENTER, CENTER);
text("GAME OVER\nScore: " + score, width / 2, height / 2);
}
function draw() {
// 如果游戏已经结束,直接返回,不再执行后续逻辑
if (!gameRunning) {
return;
}
// ===中间逻辑不变
// === 修改:敌人到达底部则游戏结束===
for (let i = enemies.length - 1; i >= 0; i--) {
enemies[i].move();
enemies[i].show();
// 到达底部的敌人
if (enemies[i].y > height) {
gameOver();
return;
}
}
// === 子弹的展现逻辑不变===
// === 修改:敌人和玩家碰撞则游戏结束 ===
for (let i = enemies.length - 1; i >= 0; i--) {
let d = dist(myShip.x, myShip.y, enemies[i].x, enemies[i].y);
let hitRadius = (myShip.width + enemies[i].size) / 2;
// 和玩家碰撞的敌人
if (d < hitRadius) {
gameOver();
return;
}
}
}
最终完整代码:
// ==============================
// 全局变量声明
// ==============================
// 用于追踪游戏是否正在进行
let gameRunning = true;
// 玩家飞船对象
let myShip;
// 敌人(小行星)数组 - 核心概念:使用数组管理多个同类对象
// 数组让我们可以轻松地批量操作所有敌人,而不需要为每个敌人单独写代码。
let enemies = [];
// 子弹数组 - 同样使用数组来管理动态生成的子弹
let bullets = [];
// 玩家得分
let score = 0;
// 用于追踪键盘按键状态的标志变量
let movingLeft = false;
let movingRight = false;
// ==============================
// p5.js 核心函数
// ==============================
function setup() {
// 创建 400x600 的画布
createCanvas(400, 600);
// 创建玩家飞船实例 - 核心概念:实例化
// 'new Player()' 会调用 Player 类的 constructor 方法,
// 并返回一个拥有该类所有属性和方法的新对象。
myShip = new Player();
}
function draw() {
// 如果游戏已经结束,直接返回,不再执行后续逻辑
if (!gameRunning) {
return;
}
// 绘制带拖尾效果的深色背景
background(10, 10, 30, 20);
// 更新并显示玩家飞船
if (myShip) {
// 根据按键状态移动飞船
if (movingLeft) {
myShip.move('left');
}
if (movingRight) {
myShip.move('right');
}
myShip.show();
}
// ==============================
// 敌人生成与管理
// ==============================
// 核心概念:使用 frameCount 实现周期性事件
// frameCount 是 p5.js 的全局变量,每帧递增1。
// '% 30 === 0' 表示每30帧(约0.5秒)执行一次。
if (frameCount % 30 === 0) {
let xPos = random(0, width);
let size = random(20, 40);
// 创建新的 Enemy 实例并添加到 enemies 数组
enemies.push(new Enemy(xPos, 0, size));
}
// 遍历 enemies 数组,更新和显示每个敌人
// 核心概念:数组遍历
// 我们使用传统的 for 循环,从后往前遍历(i--)。
// 这样做是为了安全地在循环中删除数组元素(使用 splice),
// 避免因数组长度变化而导致的索引错乱问题。
for (let i = enemies.length - 1; i >= 0; i--) {
enemies[i].move();
enemies[i].show();
// 检查敌人是否到达屏幕底部
if (enemies[i].y > height) {
gameOver();
break; // 一旦游戏结束,跳出循环
}
}
// ==============================
// 子弹管理
// ==============================
// 遍历 bullets 数组,更新和显示每个子弹
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].move();
bullets[i].show();
// 检查子弹是否飞出屏幕顶部
if (bullets[i].y < 0) {
bullets.splice(i, 1); // 从数组中移除该子弹
}
}
// ==============================
// 碰撞检测系统
// ==============================
// 1. 子弹与敌人的碰撞检测
// 我们需要检查每一个子弹是否与每一个敌人发生碰撞。使用 dist() 函数计算两点间距离,并与预设的碰撞半径比较。
for (let i = bullets.length - 1; i >= 0; i--) {
for (let j = enemies.length - 1; j >= 0; j--) {
let d = dist(bullets[i].x, bullets[i].y, enemies[j].x, enemies[j].y);
// 简化的碰撞半径:取子弹和敌人宽度的一半之和
let collisionRadius = (bullets[i].width + enemies[j].size) / 2;
if (d < collisionRadius) {
// 碰撞发生
bullets.splice(i, 1); // 移除子弹
enemies.splice(j, 1); // 移除敌人
score += 10; // 增加得分
break; // 一颗子弹只能击中一个敌人,跳出内层循环
}
}
}
// 2. 玩家与敌人的碰撞检测
for (let enemy of enemies) {
let d = dist(myShip.x, myShip.y, enemy.x, enemy.y);
let collisionRadius = (myShip.width + enemy.size) / 2;
if (d < collisionRadius) {
gameOver();
break;
}
}
// 显示得分
fill(255);
textSize(16);
textAlign(LEFT);
text("Score: " + score, 20, 30);
}
// ==============================
// 键盘事件处理
// ==============================
function keyPressed() {
// 当按键被按下时,设置移动标志
if (keyCode === LEFT_ARROW) {
movingLeft = true;
}
if (keyCode === RIGHT_ARROW) {
movingRight = true;
}
}
function keyReleased() {
// 当按键被松开时,重置移动标志
if (keyCode === LEFT_ARROW) {
movingLeft = false;
}
if (keyCode === RIGHT_ARROW) {
movingRight = false;
}
// 空格键发射子弹
if (keyCode === 32) { // 32 是空格键的 keyCode
if (myShip) {
// 子弹从飞船顶部中心发射
let bulletX = myShip.x;
let bulletY = myShip.y - myShip.height / 2;
bullets.push(new Bullet(bulletX, bulletY));
}
}
}
// ==============================
// 游戏结束函数
// ==============================
function gameOver() {
gameRunning = false;
// 显示游戏结束信息
fill(255, 0, 0);
textSize(32);
textAlign(CENTER, CENTER);
text("GAME OVER\nScore: " + score, width / 2, height / 2);
}
// ==============================
// 面向对象编程:类定义
// ==============================
// 核心概念:类(Class)是创建对象的蓝图。
// 它封装了对象的属性(数据)和方法(行为)。
// 玩家飞船类
class Player {
constructor() {
// 属性:定义对象的状态
this.x = width / 2;
this.y = height - 60;
this.width = 40;
this.height = 40;
this.color = 'cyan'; // 青色
this.speed = 6;
}
// 方法:定义对象的行为
show() {
fill(this.color);
noStroke();
// 绘制一个三角形飞船
let halfW = this.width / 2;
let halfH = this.height / 2;
beginShape();
vertex(this.x, this.y - halfH); // 顶部顶点
vertex(this.x - halfW, this.y + halfH); // 左下顶点
vertex(this.x + halfW, this.y + halfH); // 右下顶点
endShape(CLOSE);
}
move(direction) {
if (direction === 'left') {
this.x -= this.speed;
} else if (direction === 'right') {
this.x += this.speed;
}
// 使用 constrain() 限制飞船在画布内移动
this.x = constrain(this.x, this.width/2, width - this.width/2);
}
}
// 敌人类(小行星)
class Enemy {
constructor(x, y, size) {
this.x = x;
this.y = y;
this.size = size;
// 随机生成灰色调
this.color = color(random(100, 200), random(100, 200), random(100, 200));
this.speed = random(2, 3);
}
show() {
fill(this.color);
noStroke();
// 绘制方形小行星
rectMode(CENTER);
rect(this.x, this.y, this.size, this.size);
rectMode(CORNER); // 重置为默认模式,避免影响其他绘制
}
move() {
this.y += this.speed;
}
}
// 子弹类
class Bullet {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 4;
this.height = 12;
this.color = color(255, 255, 0); // 黄色
this.speed = -10; // 负值表示向上移动
}
show() {
fill(this.color);
noStroke();
rect(this.x - this.width/2, this.y, this.width, this.height);
}
move() {
this.y += this.speed;
}
}
课程总结与展望
知识回顾
在今天的课程中,我们:
- 复习了JavaScript基础:变量、数组、条件、循环、函数等核心概念
- 巩固了p5.js图形技能:基本形状绘制、颜色控制、渐变效果
- 学习了面向对象编程:工程必备:类、对象,用类创建模板,用对象创建实例,让代码模块化工程化。
- 掌握了开发流程:从需求分析到伪代码,再到完整程序,中间的错误采用调试,是解决问题的通用方法。
- 完成了综合项目:一个完整的太空射击游戏,从简单到复杂,明确了一个大项目是怎么从0到1一点一点搭建起来的。
学习建议
- 持续练习:编程技能需要通过不断练习来巩固
- 创意实验:尝试在已有代码上进一步完善、修改游戏规则,创造自己的独特版本
- 社区交流:加入p5.js社区Community,分享作品,获取反馈
- 项目扩展:基于今天的游戏,添加更多功能和创意元素
资源推荐
- p5.js官方文档:p5js.org/reference/
- The Coding Train教程:www.youtube.com/c/TheCoding…
- OpenProcessing社区:openprocessing.org/
- p5.js中文教程:p5js.org.cn/
下一步学习方向
- p5.js高级功能:着色器、3D图形、物理模拟等
- Web开发整合:将p5.js作品嵌入网页,添加交互界面
- 数据可视化:使用p5.js创建动态数据展示
- 生成艺术:探索算法艺术和创意编程的无限可能