SNKRX的前身教程,教程包括lua,love2d
熟悉lua的面向编程,lua里没有内置的类,需要用表和函数实现,这里的oop是第三方的库rxi/classic 用法
class = request "classic"
rect = class:expend() --创建类
function rect:new(posx,posy,x,y)--创建方法
self.posx = posx or 0
self.posy = posy or 0
self.x = x or 100
self.y = y or 100
end
function rect:draw()
end
function rect:fadein()
end练习 hump.timer 的 after 与 tween,写了HpRect类。
timer:tween(0.1 * self.speed, self, { rectside = { x = self.faderectside.x } }, 'in-linear', function()
timer:tween(0.1 * self.speed, self, { bkrectside = { x = self.rectside.x } }, 'in-quad', function())
end)timer = request "timer"
function love.load()
time=timer()
end
function love.update(dt)
time:update(dt)
time:after(3,function() print(love.math.random()) end)
time:every(1,function() print("已过1秒") end ,3)
timer:every(1, function printTime() end)
timer:every(1,printTime)
--以上两种用法输出结果一样,注意直接调用显名函数不需要()
--注意不能直接在love.update,love.draw调用,会一直重复调用
end
function printTime()
print("已过1秒")
end这里的after将会在过3秒和6秒时随机打印一个数,every则会每秒输出一次已过1秒总共输出3次,若没有3则会一直循环,特别注意不要在持续快速调用的函数内直接使用timer,会出现重复调用的bug,重复次数跟调用次数有关,类似love.update(),love.draw()
after
timer:after(delay,func) --(延迟dlay秒后执行,执行的匿名函数)
every
timer:every(dlay,func,count) --(延迟dlay秒后执行,执行的匿名函数,执行延迟次数)tween补间动画
hump.timer 提供了两种补间动画接口:底层接口 Timer.during()和高级接口 Timer.tween()
timer.tween(duration, subject, target, method, after, ...)有多种补间模式linear, quad, cubic, quart, quint, sine, expo, circ, back, bounce, 和 elastic.
可以自定义插值方法,详情见官方文档
during
Timer.during(delay, func, after)script
timer.script(func) --传入的方法,内置的仅有wait
timer:script(function(wait)
while true do
print("This message appears every 3 seconds.")
wait(3)
end
end)每三秒输出
new创建新计时器
timer.new()适用于挂载在敌人等实体
cancel取消计时器实例
timer.cancel(handle)
function tick()
print('tick... tock...')
end
handle = Timer.every(1, tick)
-- later
Timer.cancel(handle) -- 不是: Timer.cancel(tick)
-- using a timer instance
function tick()
print('tick... tock...')
end
handle = menuTimer:every(1, tick)
-- later
menuTimer:cancel(handle)
clear移除所有定时和周期性函数。尚未执行的函数将被丢弃。
Timer.clear()
menuTimer:clear()update
timer.update(dt)更新计时器并在达到截止时间时执行函数。在 love.update(dt) 中调用
研究comfyUI,和怎么在HugginggFace下模型,终端一直在报错= =整天都在面對CMD,PSL,WSL,该死的wsl,各种依赖一直报错,但我还是很喜欢Linux,命令行其实也挺好用的
- 重构了
HPrect
ocal HpRect = class:extend()
function HpRect:new(rectsidemax, maxhp, minhp, pos, hpcolor, bkcolor, speed)
self.maxhp = maxhp or 100
self.minhp = minhp or 0
self.currenthp = self.maxhp
self.rectsidemax = rectsidemax or { x = 200, y = 35 } --初始血量矩形大小,便于计算血量比例
--后续改用color类来表示颜色
self.hpcolor = hpcolor or { 255 / 255, 89 / 255, 89 / 255 }
self.bkcolor = bkcolor or { 226 / 255, 37 / 255, 50 / 255 }
self.bkrectside = self.rectsidemax --背景矩形大小
self.rectside = rectsidemax or { x = 200, y = 35 }
self.pos = pos or { x = 100, y = 100 }
self.speed = speed or 3 --血量动画速度
self.faderectside = { x = self.rectside.x, y = self.rectside.y } --缓冲血量
end
--扣血实现,在main.lua中调用,例如:hpRect:hurt(10) 表示扣10点血
function HpRect:hurt(damage)
if self.currenthp > self.minhp and self.rectside.x > 0 then
self.currenthp = math.max(self.currenthp - damage, self.minhp)
self.faderectside.x = (self.currenthp / self.maxhp) * self.rectsidemax.x
-- 确保目标宽度不为负数
self.faderectside.x = math.max(self.faderectside.x, 0)
if self.faderectside.x > 0 then
timer:tween(0.1 * self.speed, self, { rectside = { x = self.faderectside.x } }, 'in-linear', function()
-- tween 完成后再次强制非负,防止多次快速调用产生负值
self.rectside.x = math.max((self.rectside.x or 0), 0)
timer:tween(0.1 * self.speed, self, { bkrectside = { x = self.rectside.x } }, 'in-quad', function()
self.bkrectside.x = math.max((self.bkrectside.x or 0), 0)
end)
end)
else
self.rectside.x = 0
self.bkrectside.x = 0
end
end
end
function HpRect:draw(x, y)
if x == nil and y == nil then
love.graphics.setColor(self.bkcolor)
love.graphics.rectangle("fill", love.graphics.getWidth() / 2 - self.pos.x,
love.graphics.getHeight() / 2 - self.bkrectside.y / 2, self.bkrectside.x, self.bkrectside.y) --HP背景
love.graphics.setColor(self.hpcolor)
love.graphics.rectangle("fill", love.graphics.getWidth() / 2 - self.pos.x,
love.graphics.getHeight() / 2 - self.rectside.y / 2, self.rectside.x, self.rectside.y) --血量
else
love.graphics.setColor(self.bkcolor)
love.graphics.rectangle("fill", x, y, self.bkrectside.x, self.bkrectside.y) --HP背景
love.graphics.setColor(self.hpcolor)
love.graphics.rectangle("fill", x, y, self.rectside.x, self.rectside.y) --血量
end
end - 新增了
input类,可以直接在update()执行交互逻辑了
input使用前需要先绑定按键,按键可以绑定动作
input:bind("q", "hurt")
input:bind('move_left', {'a', 'left', 'dpleft', 'm1'}) --后续再改,支持绑定多个按键这里将q键绑定到hurt这个动作,使用时可以直接如上图所示判断hurt是否被触发,更易理解,后续将增加自定义绑键
function love.update(dt)
timer:update(dt)
if input:pressed("hurt") and hpRect.rectside.x > 0 then
hpRect:hurt(10)
end
if input:pressed("click") then
playHpRect:hurt(10)
end
end- 在
objects.lua中:给HpRect:hurt增加宽度非负约束(clamp),并在 tween 回调处再次强制 non-negative,防止在短时间内多次调用导致rectside.x < 0,但运行 Love2D 项目并按q或点击快速触发hurt(),观察rectside.x为负。
研究游戏的结构性代码,比较抽象,并非实际的应用层函数
- room
类似unity中的
Scene,游戏将在各个场景中切换,例如主菜单,初始大厅,实际游戏场景 - area
一个
Scene下的区域,并非传统的武器攻击范围,这里的area跟接近于全局掌管这个room所有游戏对象,类似在这个Scene下的次级Scene,例如在Stage这个room,物理世界引擎将挂载在FightRoom.area,一个room一般只有一个area也可以不用area
偶然看到终端美化,搞了3,4个小时在美化上, = = ,好看总是第一生产力
room和area的实际运用
- room
class = require "libraries.classic.classic"
local room = class:extend()
room.name = nil
room.type = nil
room.side = nil
-- args 是可变参数列表
function room:new(name, args)
self.name = name or "default_room"
if args then -- 可变参数赋值
for index, value in pairs(args) do
self[index] = value
end
end
end
function room:update(dt)
end
--测试用 绘制形状
function room:drawShape(mode,side, x, y)
...
endargs在这里实际上是一个表,可以批处理参数
for index, value in pairs(args) do
-- 如果当前参数是表,就合并到 room_opts 中
if type(value) == "table" then
for k, v in pairs(value) do
room_opts[k] = v
end
else
-- 如果是普通值,按索引存起来(比如额外参数)
room_opts["arg_" .. index] = value
end
endfunction GameObject:new(area, x, y, opts)
local opts = opts or {}
if opts then for k, v in pairs(opts) do self[k] = v end end --精简版 opts=args
endlua内置遍历表的方法,ipairs()和pairs(),核心区别ipairs()适用于有序数数组类似py里的数组,pairs()不管键是什么类型,全部遍历但无序
重构room类,实现room持久化,增加gotoRoom(),addRoom(),room状态检测等
-- objects/room.lua
--- 激活房间
function room:active()
print("Room "..self.name.." is now active.")
self.active = true
end
--- 取消激活
function room:deactive()
print("Room "..self.name.." is now deactive.")
self.active = false
end-- main.lua
love.load()
rooms={}
current_room=nil
--定义初始房间
deful_room = room:addRoom("room","deful_room",{leve=0,test = true})
current_room =deful_room
end
love.update(dt)
current_room
end
function addRoom(room_type, room_name, args)
rooms[room_name] = _G[room_type](room_name, args)
print("Added room: " .. room_name)
print(rooms[room_name].name, rooms[room_name].type, rooms[room_name].side)
return rooms[room_name] --返回room便于直接修改,不用再去rooms表里找
end
function gotoRoom(room_type, room_name, args)
print("Switching to room: " .. room_name)
if current_room and rooms[room_name] then
print(room_name .. " is now active.")
if current_room.deactive then
current_room:deactive()
end
current_room = rooms[room_name]
if current_room.active then
current_room:active()
end
else
current_room = addRoom(room_type, room_name, args)
print("Created and switched to new room: " .. current_room.name)
end
end
通过addRoom()添加房间到rooms,用gotoRoom()切换房间,active()与deactive()控制切换后启用当前房间,停用后台房间
补清前几天的日志
游戏对象基类,所有对象都从此继承
--onjects/gameObject
function GameObject:new(area, x, y, opts)
local opts = opts or {}
if opts then for k, v in pairs(opts) do self[k] = v end end
self.area = area
self.x, self.y = x, y
self.id = uuid()
self.dead = false
self.timer = Timer()
end
function GameObject:update(dt)
if self.timer then self.timer:update(dt) end
end
function GameObject:draw()
end
function uuid()
local fn = function(x)
local r = math.random(16) - 1
r = (x == "x") and (r + 1) or (r % 4) + 9
return ("0123456789abcdef"):sub(r, r)
end
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end自动读取类文件,love2d更新了接口,原教程使用的love.filesystem.isFile(),love.filesystem.isDirectory(file)需要替换,以下是更新过的方法
-- main.lua
love.load()
local object_files = {}
object_files = recursiveEnumerate("objects", object_files)
requireFiles(object_files)
end
function recursiveEnumerate(folder, file_list) -- 递归枚举文件夹中的所有文件
-- 容错:确保 file_list 是有效表(避免传入 nil 导致报错)
file_list = file_list or {}
-- 容错:确保 folder 路径合法(首尾无多余斜杠)
folder = folder:gsub("/+$", "")
-- 获取文件夹下的所有项(文件/子文件夹)
local items = love.filesystem.getDirectoryItems(folder)
for _, item in ipairs(items) do
local file_path = folder .. '/' .. item
-- 核心修改:用 getInfo 获取文件/目录信息(替代过时函数)
local file_info = love.filesystem.getInfo(file_path)
if not file_info then
-- 跳过不存在/无权限的项(增强健壮性)
goto continue
end
if file_info.type == "file" then
-- 是文件:加入列表
table.insert(file_list, file_path)
elseif file_info.type == "directory" then
-- 是目录:递归枚举
recursiveEnumerate(file_path, file_list)
end
::continue:: -- Lua 标签,用于跳过无效项
end
return file_list -- 返回结果(方便调用时直接获取)
end
function requireFiles(files) --批量require文件
for _, file in ipairs(files) do
local className = file:match("([^/]+)%.lua$")
local file = file:sub(1, -5)
_G[className] = require(file)
end
end
实现在room.area中进行逻辑,制作了简短的场景加载动画,实现在一个房间的区域里生成游戏对象,实现切换房间可选择性留存房间下区域内的游戏对象,视频中的每个圆都是继承自GameObject的游戏对象CiecleFade,在Stage中的area动态加载
CiecleFade
-- objects/GameObject
CircleFade = GameObject:extend()
function CircleFade:new(area, x, y, opts)
wx = love.graphics.getWidth()
wy = love.graphics.getHeight()
self.side = 0
self.timer = Timer:new()
CircleFade.super.new(self, area, x, y, opts)
self:deadLoop()
end
--圆形生成参数
function CircleFade:deadLoop()
if self.side == 0 then
timer:tween(random(1, 2.5), self, { side = random(20, 35) }, 'in-out-quad', function()
timer:after(0.45, function()
timer:tween(2, self, { side = 0 }, 'in-out-quad', function()
timer:after(0.1, function() self.dead = true end)
end)
end)
end)
else
self.side = 0
timer:tween(5, self, { side = wx*1.2 }, 'in-out-quad', function()
timer:tween(0.9,self,{side = 0},'in-out-quad',function ()
timer:after(0.1, function() self.dead = true end)
end)
end)
end
end
function CircleFade:update(dt)
CircleFade.super.update(self, dt)
self.timer:update(dt)
end
function CircleFade:draw()
love.graphics.circle("fill", self.x, self.y, self.side)
endSatge
-- objects/Room
Stage = class:extend()
function Stage:new()
self.area = Area(self)
self.timer = Timer()
end
function Stage:update(dt)
self.area:update(dt)
self.timer:update(dt)
end
function Stage:draw()
self.area:draw()
end
function Stage:active()
self:draw()
self.area:active()
print("Stage active")
self.timer:every(0.005, function()
self.area:addGameObject('CircleFade', random(0, 800), random(0, 600))
self.area:addGameObject('CircleFade', random(0, 800), random(0, 600))
self.area:addGameObject('CircleFade', random(0, 800), random(0, 600))
self.area:addGameObject('CircleFade', random(0, 800), random(0, 600))
end, 1000)
self.timer:after(0.7, function () self.area:addGameObject("CircleFade",wx/2,wy/2,{side = wx})
end)
end
function Stage:deactive()
self.area:deactive()
self.area.game_objects={}
end
function random(min, max)
if not max then -- if max is nil then it means only one value was passed in
return love.math.random()*min
else
if min > max then min, max = max, min end
return love.math.random()*(max - min) + min
end
end为Area添加启用判断,利于持久化room
function Area:update(dt)
--循环采用倒序方式,从列表末尾向开头遍历。这是因为如果在正向遍历 Lua 表时删除元素会导致某些元素被跳过
if self.active ==true then
self.timer:update(dt)
end
for i = #self.game_objects, 1, -1 do
local game_object = self.game_objects[i]
game_object:update(dt)
if game_object.dead then table.remove(self.game_objects, i)end
end
end
function Area:draw()
for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end
function Area:active()
self.isactive=true
end
function Area:deactive()
self.isactive=false
end
function Area:addGameObject(game_object_type, x, y, opts)
local opts = opts or {}
local game_object = _G[game_object_type](self, x or 0, y or 0, opts)
table.insert(self.game_objects, game_object)
return game_object
endClaude制作微信小程序
Area与room的练习
Timer
调用Timer要注意及时销毁handle,来回激活room之前调用的Timer的句柄会重复调用
self.t:after(2.5, function()
Loop_every = self.t:every(random(0.5, 1), function() --Loop_every是被赋值的句柄
table.remove(self.area.game_objects, random(1, #self.area.game_objects))
if #self.area.game_objects == 0 then
self.t:cancel(Loop_every)
Loop()
end
end)
end)Area
新增function Area:queryCircleArea(x, y, radius, needClass),function getDistance(...)
--接收一个 x 、 y 位置坐标,一个 radius 以及包含目标类名称的字符串列表。
--然后返回以 x, y 位置为中心、半径为 radius 的圆形区域内属于这些类的所有对象。
-- objects = area:queryCircleArea(100, 100, 50, {'Enemy', 'Projectile'})
--needClass,gameobject.class都可以是表的形式
function Area:queryCircleArea(x, y, radius, needClass)
...
end--获取对象的xy,返回距离 重载obj,x1,y1 obj1,obj2 x1,y1,x2,y2
function getDistance(...)
endArea
新增function Area:getClosestGameObject(x,y,radius,needClass,n)
--与 queryCircleArea函数类似,但只返回最近的或n个对象{对象,距离},n=nil返回最近的一个
function Area:getClosestGameObject(x,y,radius,needClass,n)
local out = {}
if not needClass then
for _, value in ipairs(self.game_objects) do
local ds =getDistance(value, x, y)
if ds <= radius then
table.insert(out, {value,ds}) -- {{Rect,15},{Rect,20}}
-- print("add")
end
end
table.sort(out, function (a, b)
return a[2]<b[2]
end)
if n==nil then
return out[1]
end
return table.slice(out,1,n)
end
for _, value in ipairs(self.game_objects) do
local ds =getDistance(value, x, y)
if ds <= radius and checkClass(value,needClass) then
table.insert(out, {value,ds})
-- print("add")
end
end
table.sort(out, function (a, b)
return a[2]<b[2]
end)
if n==nil then
return out[1]
end
return table.slice(out,1,n)
end重构function Area:queryCircleArea(x, y, radius, needClass),将checkClass()独立封装出来
function Area:queryCircleArea(x, y, radius, needClass)
local out = {}
if not needClass then
for _, value in ipairs(self.game_objects) do
if getDistance(value, x, y) <= radius then
table.insert(out, value)
print("add")
end
end
return out
end
for _, value in ipairs(self.game_objects) do
if getDistance(value, x, y) <= radius and checkClass(value,needClass) then
table.insert(out, value)
print("add")
end
end
return out
end

