Skip to content
YuanYuSQPublic

About

学习Lua及LOVE2D游戏开发

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lua和love2d引擎

SNKRX的前身教程,教程包括lua,love2d

日志

2026/1/12

熟悉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

2026/1/14

练习 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

  • 这里的timer是hump其中的一个类,timer内置了许多方法,最常用的有after,tween,every,during.
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) 中调用


2026/1/18

研究comfyUI,和怎么在HugginggFace下模型,终端一直在报错= =整天都在面對CMD,PSL,WSL,该死的wsl,各种依赖一直报错,但我还是很喜欢Linux,命令行其实也挺好用的


2026/1/19

  • 重构了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 为负。

2026/1/20

研究游戏的结构性代码,比较抽象,并非实际的应用层函数

  • room 类似unity中的Scene,游戏将在各个场景中切换,例如主菜单,初始大厅,实际游戏场景
  • area 一个Scene下的区域,并非传统的武器攻击范围,这里的area跟接近于全局掌管这个room所有游戏对象,类似在这个Scene下的次级Scene,例如在Stage这个room,物理世界引擎将挂载在FightRoom.area,一个room一般只有一个area也可以不用area

2026/1/21

偶然看到终端美化,搞了3,4个小时在美化上, = = ,好看总是第一生产力

  • 美化前 美化前

  • 美化后 美化后


2026/1/22

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)
   ...
end

args在这里实际上是一个表,可以批处理参数

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
    end
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  --精简版 opts=args
end

lua内置遍历表的方法,ipairs()和pairs(),核心区别ipairs()适用于有序数数组类似py里的数组,pairs()不管键是什么类型,全部遍历但无序


2026/1/23

重构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()控制切换后启用当前房间,停用后台房间


2026/1/24

补清前几天的日志


2026/1/25

游戏对象基类,所有对象都从此继承

--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

2026/1/26

实现在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)
end
  • Satge
-- 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
end

2026/1/27~29

Claude制作微信小程序


2026/1/30

Area与room的练习


2026/1/31

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)

2026/2/1

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(...)
end

2026/2/3

Area 新增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

About

学习Lua及LOVE2D游戏开发

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages