【Skynet 入门实战练习】游戏模块划分 | 基础功能模块 | timer 定时器模块 | logger 日志服务模块

文章目录

    • 游戏模块
    • 基础功能模块
      • 定时器模块
      • 日志模块
      • 通用模块

游戏模块

游戏从逻辑方面可以分为下面几个模块:

  • 注册和登录
  • 网络协议
  • 数据库
  • 玩法逻辑
  • 其他通用模块

除了逻辑划分,还有几个重要的工具类模块:

  • Excel 配置导表工具
  • GM 指令
  • 测试机器人
  • 服务器打包部署工具

本节先来实现几个通用的基础功能模块。

基础功能模块

定时器模块

在什么场景下,我们会要使用到定时器?

  • 每日任务的重置,比如游戏在每天的 0 点,需要定时进行刷新
  • 登录流程的超时机制,对于长时间未通过验证的连接,需要踢客户端下线,避免占用服务端资源
  • 活动结算,在定期活动结束后,需要给所有用户发放结算奖励

服务端和客户端都可以实现定时器逻辑,一般涉及到全服玩家的定时器,需要服务器来实现,针对个人玩家的定时器可以交给客户端来实现。


skynet 中,通过 skynet.timeout(time, func) 实现定时任务,skynet 基本的时间单位是 10ms,即会在 0.01s 后执行一次 func 函数。

参考:https://github.com/cloudwu/skynet/wiki/LuaAPI

skynet.timeout(ti, func) 让框架在 ti 个单位时间后,调用 func 这个函数。这不是一个阻塞 API ,当前 coroutine 会继续向下运行,而 func 将来会在新的 coroutine 中执行。

skynet 的定时器实现的非常高效,所以一般不用太担心性能问题。不过,如果你的服务想大量使用定时器的话,可以考虑一个更好的方法:即在一个service里,尽量只使用一个 skynet.timeout ,用它来触发自己的定时事件模块。这样可以减少大量从框架发送到服务的消息数量。毕竟一个服务在同一个单位时间能处理的外部消息数量是有限的。

由此,考虑自己实现一个用于定时触发事件的定时器模块,并且不需要这么高的精度,采用以秒为单位实现定时器。

定时器模块实现架构: skynet.timeout 实现循环定时器,每秒循环一次,查看并执行这一秒对应的回调函数。定时器 id 采用自增唯一映射每个定时回调函数。注册回调函数时,会计算将要执行的秒数,存入对应的回调函数表。


基础变量以及模块初始化

local _M = {} 

local is_init = false   -- 标记模块是否初始化
local timer_inc_id = 1  -- 定时器的自增 ID
local cur_frame = 0     -- 当前帧,一帧对应一秒
local cur_timestamp = 0 -- 当前时间戳,运行到的秒数
local timer_size = 0    -- 定时器数量
local frame_size = 0    -- 帧数量

local timer2frame = {}  -- 定时器ID 映射 帧
local frame2cbs = {}    -- 帧 映射 多个回调任务
--[[
    frame: {
        timers: {
            timerid: { sec, cb, args, is_repeat },
            timerid: { sec, cb, args, is_repeat }
        }, 
        size: 1
    }
]]

if not is_init then 
    is_init = true -- 初始化定时器模块
    skynet.timeout(100, main_loop)
end 

return _M 
  • is_init:用于标记模块是否初始化,即生成一次循环定时器,定时每秒执行 main_loop 函数
  • timer_inc_id:定时器的唯一标识 ID,每个定时器创建,都会自增
  • cur_frame:记录当前循环是对应哪一帧,随着循环自增
  • cur_timestamp:当前循环时间戳
  • timer_sizeframe_size:维护的定时器数量和帧数量
  • timer2frame:定时器 ID 对应的帧
  • frame2cbs:帧对应的回调函数表

回调函数表的结构 frame2cbs

frame: {
    timers: {
        timerid: { sec, cb, args, is_repeat },
        timerid: { sec, cb, args, is_repeat }
    }, 
    size: 1
}

每帧对应回调函数表,有 timerssize 两个字段,size 维护当前回调函数个数,timers 则是实际的回调函数表,以定时器 ID 映射对应的回调函数。

每个回调函数都存储 seccbargsis_repeat 四个字段,表示 sec 秒后执行 cb 函数,携带 args 参数, is_repeat 表示是否是一个循环任务。


下面看每帧执行的函数 main_loop

local function now() 
    return skynet.time() // 1 -- 截断小数:.0
end     
-- 逐帧执行
local function main_loop()
    skynet.timeout(100, main_loop)
    cur_timestamp = now()
    cur_frame = cur_frame + 1

    -- 当前没有定时器任务
    if timer_size <= 0 then return end 

    -- 当前帧对应的回调任务
    local cbs = frame2cbs[cur_frame]
    if not cbs then return end 

    -- 当前帧的回调任务数量为0
    if cbs.size <= 0 then 
        frame2cbs[cur_frame] = nil 
        frame_size = frame_size - 1 -- 该帧执行完毕
        return 
    end 

    -- task: {sec, cb, args, is_repeat}
    for timerid, task in pairs(cbs.timers) do 
        local f = task[2] 
        local args = task[3]
        local ok, err = xpcall(f, traceback, unpack(args, 1, args.n))
        if not ok then 
            logger.error("timer", "crontab is run in error:", err)
        end 
        del_timer(timerid) -- 执行成功与否都需要删掉当前这个定时器

        local is_repeat = task[4]
        if is_repeat then 
            local sec = task[1]
            init_timer(timerid, sec, f, args, is_repeat)
        end 
    end 

    -- 当前这一帧所有任务执行完,并且这一帧没有删(双重保障(del_timer)),删掉当前帧
    if frame2cbs[cur_frame] then 
        frame2cbs[cur_frame] = nil 
        frame_size = frame_size - 1
    end 
end 

这里在入口处,我们就立即需要执行 skynet.timeout(100, main_loop),实现循环定时,并且没有多余其他操作,保证一下秒定时的准确。

skynet.time():当前 UTC 时间(单位是秒, 精度是 ms)

主要逻辑:判断当前帧是否有任务,有则执行 frame2cbs[cur_frame].timers 回调函数表中的回调函数,执行完后进行删除和判断该回调是否是循环定时任务,是则重新创建该回调的新定时器。


再来看定时器的创建和删除逻辑

init_timer

local function init_timer(id, sec, f, args, is_repeat)
    -- 第一步:定时器 id 映射 帧
    local offset_frame = sec -- sec 帧后开始当前任务 
    -- 矫正帧数
    if now() > cur_timestamp then 
        offset_frame = offset_frame + 1
    end 
    -- 实际计算执行帧
    local fix_frame = cur_frame + offset_frame

    -- 第二步:该帧 映射 定时器任务
    local cbs = frame2cbs[fix_frame]
    if not cbs then 
        -- 创新当前帧的任务集
        cbs = { timers = {}, size = 1 }
        frame2cbs[fix_frame] = cbs 
        frame_size = frame_size + 1 
    else 
        cbs.size = cbs.size + 1
    end 

    cbs.timers[id] = {sec, f, args, is_repeat}
    timer2frame[id] = fix_frame

    timer_size = timer_size + 1

    if timer_size >= 500 then 
        logger.warn("timer", "timer is too many!")
    end 
end 

创建定时器任务,对应需要修改 frame2cbstimer2frame 表。回调函数加入当前帧的回调表中,回调的定时器ID映射当前帧,一并维护一下定时器和帧的数量统计。

在函数的开始,我们进行了对帧的校正。保证回调任务在未来帧中执行,而不会在当前帧中继续添加任务。

del_timer

-- 删除定时器
local function del_timer(id) 
    -- 获取定时器id 映射 帧
    local frame = timer2frame[id]
    if not frame then return end 

    -- 获取该帧对应的任务
    local cbs = frame2cbs[frame]
    if not cbs or not cbs.timers then return end 

    -- 如果这个帧中的定时器任务存在
    if cbs.timers[id] then 
        cbs.timers[id] = nil -- 删除该定时器任务
        cbs.size = cbs.size - 1 -- 当前帧的任务数 -1
    end 

    -- 当前删掉了这一帧的最后一个定时器任务
    if cbs.size == 0 then 
        frame2cbs[frame] = nil -- 置空
        frame_size = frame_size - 1 -- 帧数 -1 
    end 

    -- 当前定时器id对应的帧置空,且定时器数量 -1
    timer2frame[id] = nil 
    timer_size = timer_size - 1
end 

删除定时器逻辑很好理解,传入定时器 ID,找到 ID 对应的帧,看该帧中是否存在这个任务,存在就删除并维护帧数和定时器数量。


接口实现:

-- 新增定时器 timer,sec 秒后执行函数 f
-- 返回定时器 ID
function _M.timeout(sec, f, ...)
    assert(sec > 0)
    timer_inc_id = timer_inc_id + 1
    init_timer(timer_inc_id, sec, f, pack(...), false)
    return timer_inc_id
end 

function _M.timeout_repeat(sec, f, ...) 
    assert(sec > 0)
    timer_inc_id = timer_inc_id + 1
    init_timer(timer_inc_id, sec, f, pack(...), true)
    return timer_inc_id
end 

-- 取消定时器任务
function _M.cancel(id)
    del_timer(id)
end 

-- 检查定时器是否存在
function _M.exist(id)
    if timer2frame[id] then return true end 
    return false 
end 

-- 获取定时器还有多久执行
function _M.get_remain(id)
    local frame = timer2frame[id] 
    if frame then 
        return frame - cur_frame
    end 
    return -1
end 

完整代码:timer.lua


日志模块

日志系统一般分为 4 个等级:

  • DEBUG:调试用的日志,线上运行时屏蔽不输出
  • INFO:普通日志,线上运行时输出,流程的关键步骤都需要有 INFO 日志
  • WARN:数据异常,但不影响正常流程的时候输出
  • ERROR:数据异常,且需要人工处理的时候输出

日志服务模块配置如下:

-- log conf
logger = "log"
logservice = "snlua"
logpath = "log"
logtag = "game"
-- debug | info | warn | error 
log_level = "debug"

参考官方 wiki

logger 它决定了 skynet 内建的 skynet_error 这个 C API 将信息输出到什么文件中。如果 logger 配置为 nil ,将输出到标准输出。你可以配置一个文件名来将信息记录在特定文件中。

logservice 默认为 "logger" ,你可以配置为你定制的 log 服务(比如加上时间戳等更多信息)。可以参考 service_logger.c 来实现它。注:如果你希望用 lua 来编写这个服务,可以在这里填写 snlua ,然后在 logger 配置具体的 lua 服务的名字。在 examples 目录下,有 config.userlog 这个范例可供参考。

配置中,指定 loggerlog.lua 这个日志服务,logservicesnlua 表示这个日志服务是 lua 服务。其余的三个参数作为键值对存储在配置中,用于实现服务模块时取出使用。logpath 指定为日志存放的目录路径,logtag 指定为日志进程标识,log_level 可选四种日志级别。

这里先来看日志模块:lualib/logger.lua

local skynet = require "skynet"

local loglevel = {
	debug = 0,
	info = 1,
	warn = 2,
	error = 3,
}

local logger = {
	_level = nil,
	_fmt = "[%s] [%s] %s", -- [info] [label] msg
	_fmt2 = "[%s] [%s %s] %s", --[info] [label labeldata] msg
}

local function init_log_level()
	if not logger._level then
		local level = skynet.getenv "log_level"
		
		local default_level = loglevel.debug
		local val

		if not level or not loglevel[level] then
			val = default_level
		else
			val = loglevel[level]
		end

		logger._level = val
	end
end

function logger.set_log_level(level)
	local val = loglevel.debug

	if level and loglevel[level] then
		val = loglevel[level]
	end

	logger._level = val
end

local function formatmsg(loglevel, label, labeldata, args)
	local args_len = #args

	if args_len > 0 then
		for k, v in pairs(args) do
			v = tostring(v)
			args[k] = v
		end

		args = table.concat(args, " ")
	else
		args = ""
	end

	local msg
	local fmt = logger._fmt
	if labeldata ~= nil then
		fmt = logger._fmt2
		msg = string.format(fmt, loglevel, label, labeldata, args)
	else
		msg = string.format(fmt, loglevel, label, args)
	end

	return msg
end

--[[
logger.debug("map", "user", 1024, "entered this map")
logger.debug2("map", 1, "user", 2048, "leaved this map")
]]
function logger.debug(label, ...)
	if logger._level <= loglevel.debug then
		local args = {...}
		local msg = formatmsg("debug", label, nil, args)

		skynet.error(msg)
	end
end
function logger.debug2(label, labeldata, ...)
	if logger._level <= loglevel.debug then
		local args = {...}
		local msg = formatmsg("debug", label, labeldata, args)

		skynet.error(msg)
	end
end

function logger.info(label, ...)
	if logger._level <= loglevel.info then
		local args = {...}
		local msg = formatmsg("info", label, nil, args)

		skynet.error(msg)
	end
end
function logger.info2(label, labeldata, ...)
	if logger._level <= loglevel.info then
		local args = {...}
		local msg = formatmsg("info", label, labeldata, args)

		skynet.error(msg)
	end
end

function logger.warn(label, ...)
	if logger._level <= loglevel.warn then
		local args = {...}
		local msg = formatmsg("warn", label, nil, args)

		skynet.error(msg)
	end
end
function logger.warn2(label, labeldata, ...)
	if logger._level <= loglevel.warn then
		local args = {...}
		local msg = formatmsg("warn", label, labeldata, args)

		skynet.error(msg)
	end
end

function logger.error(label, ...)
	if logger._level <= loglevel.error then
		local args = {...}
		local msg = formatmsg("error", label, nil, args, debug.traceback())

		skynet.error(msg)
	end
end
function logger.error2(label, labeldata, ...)
	if logger._level <= loglevel.error then
		local args = {...}
		local msg = formatmsg("error", label, labeldata, args, debug.traceback())

		skynet.error(msg)
	end
end

skynet.init(init_log_level)

return logger

这个日志模块主要暴露的四个接口分别对应四个日志等级,并且只有当前日志等级 log_level 低于当前 API 对应的等级才可以输出。如果程序测试阶段,那么指定 debug 级,就会获得所有日志。如果程序上线指定 error 级,那么只会关注到最高级别的错误日志。

error 等级日志额外输出了调用堆栈,方便查看错误问题所在的位置。

skynet.init:若服务尚未初始化完成,则注册一个函数等服务初始化阶段再执行;若服务已经初始化完成,则立刻运行该函数。

下面再来看一下日志服务代码:service/log.lua

local skynet = require "skynet"
require "skynet.manager"
local time = require "utils.time"

-- 日志目录
local logpath = skynet.getenv("logpath") or "log"
-- 日志文件名
local logtag  = skynet.getenv("logtag") or "game"
local logfilename = string.format("%s/%s.log", logpath, logtag)
local logfile = io.open(logfilename, "a+")

-- 写文件
local function write_log(file, str) 
    file:write(str, "\n")
    file:flush()

    print(str)
end 

-- 切割日志文件,重新打开日志
local function reopen_log() 
    -- 下一天零点再次执行
    local future = time.get_next_zero() - time.get_current_sec()
    skynet.timeout(future * 100, reopen_log)

    if logfile then logfile:close() end 

    local date_name = os.date("%Y%m%d%H%M%S", time.get_current_sec())
    local newname = string.format("%s/%s-%s.log", logpath, logtag, date_name)
    os.rename(logfilename, newname) -- logfilename文件内容剪切到newname文件
    logfile = io.open(logfilename, "a+") -- 重新持有logfilename文件
end 

-- 注册日志服务处理函数
skynet.register_protocol {
    name = "text", 
    id = skynet.PTYPE_TEXT, 
    unpack = skynet.tostring, 
    dispatch = function(_, source, str)
        local now = time.get_current_time()
        str = string.format("[%08x][%s] %s", source, now, str)
        write_log(logfile, str)
    end 
}

-- 捕捉sighup信号(kill -l) 执行安全关服逻辑
skynet.register_protocol {
    name = "SYSTEM", 
    id = skynet.PTYPE_SYSTEM, 
    unpack = function(...) return ... end,
    dispatch = function()
        -- 执行必要服务的安全退出操作

        skynet.sleep(100)
        skynet.abort()
    end 
}

local CMD = {} 

skynet.start(function()
    skynet.register(".log")
    skynet.dispatch("lua", function(_, _, cmd, ...)
        local f = CMD[cmd]
        if f then 
            skynet.ret(skynet.pack(f(...)))
        else 
            skynet.error(string.format("invalid command: [%s]", cmd))
        end
    end)

    local ok, msg = pcall(reopen_log)
    if not ok then 
        print(msg)
    end 
end)

日志服务已经在配置中指定,logger = "log"、logservice = "snlua",不需要自行启动这个日志服务。且项目中所有 skynet.error API 输出的内容都被定向到了日志文件中,而不是输出在控制台。便于调试,write_log 写日志函数,最后调用了 print 打印日志到了终端。

通过注册 skynet.PTYPE_TEXT 文本类型消息,那么项目中的 skynet.error 输出的日志都会经过本日志服务进行分发处理,由此在分发函数 dispatch = function(_, source, str) end 中处理日志消息,对所有的日志消息进行格式化的美观输出。

日志服务工作原理可以参考文章:https://www.jianshu.com/p/351ac2cfd98c/ ,本系列 skynet 偏原理性的东西不做深入讲解。

在这里插入图片描述

日志服务如果不做切割,全部放在一个文件中会导致日志文件日益增大,这里实现 reopen_log 函数,通过 skynet.timeout 定时每天零点对日志进行切割,包括在服务重启时,也会对上次的日志文件 game.log 进行分割处理。

在这里插入图片描述

日志服务还注册了一种消息类型,skynet.PTYPE_SYSTEM,用来接收 kill -1 命令的信号,触发保存数据的逻辑,待后续实现了缓存模块在完善。


通用模块

同样在本章节,继续实现几个通用模块,细心的小伙伴应该注意到了,在实现日志模块、日志服务时,都有导入 utils.time 这个处理时间的一个模块。

下图是目前的模块 lualib 文件夹的结构:

在这里插入图片描述

  • time.lua
local skynet = require "skynet"

local _M = {}

-- 一秒只转一次时间戳
local last_sec 
local current_str  

-- 获取当前时间戳
function _M.get_current_sec()
    return math.floor(skynet.time())
end

-- 获取下一天零点的时间戳
function _M.get_next_zero(cur_time, zero_point)
    zero_point = zero_point or 0 
    cur_time = cur_time or _M.get_current_sec() 
    local t = os.date("*t", cur_time) 
    if t.hour >= zero_point then 
        t = os.date("*t", cur_time + 24 * 3600) 
    end 
    local zero_date = {
        year = t.year, 
        month = t.month, 
        day = t.day, 
        hour = zero_point,
        min = 0, 
        sec = 0,
    }
    return os.time(zero_date)
end 

-- 获取当前可视化时间
function _M.get_current_time() 
    local cur = _M.get_current_sec()
    if last_sec ~= cur then 
        current_str = os.date("%Y-%m-%d %H:%M:%S", cur)
        last_sec = cur 
    end
    return current_str
end

return _M 

目前实现了三个接口:

  • get_current_sec:获取当前时间戳
  • get_current_time:获取当前可视化时间
  • get_next_zero:获取下一天零点时间戳,可以自定义项目的刷新时间 zero_point

其中有一个小优化是 last_sec,current_str 设置上一秒时间戳,与当前可视化时间变量,保证一秒只会转换一次。

os.time、os.date 的使用参考 lua 手册


  • table.lua
local string = require "string"

local _M = {}

function _M.dump(t) 
    local print_r_cache = {}
    local function sub_print_table(t, indent)
        if (print_r_cache[tostring(t)]) then
            print(indent .. "*" .. tostring(t))
        else
            print_r_cache[tostring(t)] = true
            if (type(t) == "table") then
                for pos, val in pairs(t) do
                    if (type(val) == "table") then
                        print(indent .. "[" .. pos .. "] => " .. tostring(t) .. " {")
                        sub_print_table(val, indent .. string.rep(" ", string.len(pos) + 8))
                        print(indent .. string.rep(" ", string.len(pos) + 6) .. "}")
                    elseif (type(val) == "string") then
                        print(indent .. "[" .. pos .. '] => "' .. val .. '"')
                    else
                        print(indent .. "[" .. pos .. "] => " .. tostring(val))
                    end
                end
            else
                print(indent .. tostring(t))
            end
        end
    end
    if (type(t) == "table") then
        print(tostring(t) .. " {")
        sub_print_table(t, "  ")
        print("}")
    else
        sub_print_table(t, "  ")
    end
    print()
end

return _M 
  • string.lua
local string = require "string"

local _M = {}

function _M.split(str, sep) 
    local arr = {}
	local i = 1
	for s in string.gmatch(str, "([^" .. sep .. "]+)") do
		arr[i] = s
		i = i + 1
	end
	return arr
end

return _M 

目前 table 模块仅实现了 dump 接口,对表的美化输出, string 模块仅实现了对字符串的分割转表,有需求在自定义添加更多的功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/183342.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【CCF-PTA】第03届Scratch第01题 -- 梦醒时分

梦醒时分 【题目描述】 睡眠是人体正常的生理需要&#xff0c;同年龄男女睡眠时间无明显差别&#xff0c;一般是8小时左右。居家的小明作息生活很规律&#xff0c;晚上11点睡觉&#xff0c;早晨7点起床学习。请你编写程序来判断&#xff0c;每周&#xff08;共168小时&#x…

Elasticsearch:ES|QL 函数及操作符

如果你对 ES|QL 还不是很熟悉的话&#xff0c;请阅读之前的文章 “Elasticsearch&#xff1a;ES|QL 查询语言简介​​​​​​​”。ES|QL 提供了一整套用于处理数据的函数和运算符。 功能分为以下几类&#xff1a; 目录 ES|QL 聚合函数 AVG COUNT COUNT_DISTINCT 计数为近…

【计算机网络笔记】数据链路层概述

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

IAR为恩智浦S32M2提供全面支持,提升电机控制能力

IAR Embedded Workbench for Arm已全面支持恩智浦最新的S32系列&#xff0c;可加速软件定义汽车的车身和舒适性应用的开发 瑞典乌普萨拉&#xff0c;2023年11月22日 – 嵌入式开发软件和服务的全球领导者IAR现已全面支持恩智浦半导体&#xff08;NXP Semiconductors&#xff0…

好工具|datamap,一个好用的地图可视化Excel插件,在Excel中实现地理编码、拾取坐标

在做VRP相关研究的时候&#xff0c;需要对地图数据做很多处理&#xff0c;比如地理编码&#xff0c;根据“重庆市沙坪坝区沙正街174号”这样的一个文本地址知道他的经纬度&#xff1b;再比如绘制一些散点图&#xff0c;根据某个位置的经纬度在地图上把它标注出来。还有有的时候…

Redis-Redis高可用集群之水平扩展

Redis3.0以后的版本虽然有了集群功能&#xff0c;提供了比之前版本的哨兵模式更高的性能与可用性&#xff0c;但是集群的水平扩展却比较麻烦&#xff0c;今天就来带大家看看redis高可用集群如何做水平扩展&#xff0c;原始集群(见下图)由6个节点组成&#xff0c;6个节点分布在三…

一致性 Hash 算法 Hash 环发生偏移怎么解决

本篇是对文章《一文彻底读懂一致性哈希算法》的重写&#xff0c;图文并茂&#xff0c;篇幅较长&#xff0c;欢迎阅读完提供宝贵的建议&#xff0c;一起提升文章质量。如果感觉不错不要忘记点赞、关注、转发哦。原文链接&#xff1a; 《一文彻底读懂一致性Hash 算法》 通过阅读本…

界面组件Telerik UI for WinForms中文教程 - 创建明暗模式的桌面应用

黑暗模式现在在很多应用程序中都挺常见的&#xff0c;但如何在桌面应用程序中实现它呢&#xff1f;这很简单&#xff0c;本文将为大家介绍如何使用一个类和命令行调用来实现&#xff01; Telerik UI for WinForms拥有适用Windows Forms的110多个令人惊叹的UI控件。所有的UI fo…

绝地求生:PGC 2023 赛事直播期间最高可获:2000万G-Coins,你还不来吗?

今年PGC直播期间将有最高2000万G-Coin掉落&#xff0c;究竟花落谁家咱们拭目以待 公告原文&#xff1a;Watch PGC 2023 Live And Earn G-Coins! 如何赚取高额G-Coin&#xff1f; Throughout the PGC 2023, an astounding 20,000,000 G-Coins will be up for grabs as part of …

关于「光学神经网络」的一切:理论、应用与发展

/目录/ 一、线性运算的光学实现 1.1. 光学矩阵乘法器 1.2. 光的衍射实现线性运行 1.3. 基于Rayleigh-Sommerfeld方程的实现方法 1.4. 基于傅立叶变换的实现 1.5. 通过光干涉实现线性操作 1.6. 光的散射实现线性运行 1.7. 波分复用&#xff08;WDM&#xff09;实现线性运…

OpenCV快速入门:图像分析——傅里叶变换、积分图像

文章目录 前言一、傅里叶变换1.1 离散傅里叶变换1.1.1 离散傅里叶变换原理1.1.2 离散傅里叶变换公式1.1.3 代码实现1.1.4 cv2.dft 函数解析 1.2 傅里叶变换进行卷积1.2.1 傅里叶变换卷积原理1.2.2 傅里叶变换卷积公式1.2.3 代码实现1.2.4 cv2.mulSpectrums 函数解析 1.3 离散余…

Unity机器学习 ML-Agents第一个例子

上一节我们安装了机器学习mlagents的开发环境&#xff0c;本节我们创建第一个例子&#xff0c;了解什么是机器学习。 我们的例子很简单&#xff0c;就是让机器人自主移动到目标位置&#xff0c;不能移动到地板范围外。 首先我们来简单的了解以下机器学习的过程。 机器学习的过…

读像火箭科学家一样思考笔记07_探月思维

1. 挑战“不可能”的科学与企业 1.1. 互联网 1.1.1. 和电网一样具有革命性&#xff0c;一旦你插上电源&#xff0c;就能让自己的生活充满活力 1.1.2. 互联网的接入可以帮助人们摆脱贫困&#xff0c;拯救生命 1.1.3. 互联网还可以提供与天气相关的信息 1.2. 用廉价、可靠的…

搭配:基于OpenCV的边缘检测实战

引言 计算机中的目标检测与人类识别物体的方式相似。作为人类&#xff0c;我们可以分辨出狗的形象&#xff0c;因为狗的特征是独特的。尾巴、形状、鼻子、舌头等特征综合在一起&#xff0c;帮助我们把狗和牛区分开来。 同样&#xff0c;计算机能够通过检测与估计物体的结构和性…

【Django使用】10大章31模块md文档,第5篇:Django模板和数据库使用

当你考虑开发现代化、高效且可扩展的网站和Web应用时&#xff0c;Django是一个强大的选择。Django是一个流行的开源Python Web框架&#xff0c;它提供了一个坚实的基础&#xff0c;帮助开发者快速构建功能丰富且高度定制的Web应用 全套Django笔记直接地址&#xff1a; 请移步这…

【Docker】从零开始:8.Docker命令:Commit提交命令

【Docker】从零开始&#xff1a;8.Docker命令:Commit命令 基本概念镜像镜像分层什么是镜像分层为什么 Docker 镜像要采用这种分层结构 本章要点commit 命令命令格式docker commit 操作参数实例演示1.下载一个新的ubuntu镜像2.运行容器3.查看并安装vim4.退出容器5提交自己的镜像…

51单片机应用从零开始(八)·循环语句(for循环、while 语句、do‐while 语句)

51单片机应用从零开始&#xff08;七&#xff09;循环语句&#xff08;if语句&#xff0c;swtich语句&#xff09;-CSDN博客 目录 1. 用for 语句控制蜂鸣器鸣笛次数 2. 用while 语句控制 LED 3. 用 do‐while 语句控制 P0 口 8 位 LED 流水点亮 1. 用for 语句控制蜂鸣器鸣笛…

009 OpenCV 二值化 threshold

一、环境 本文使用环境为&#xff1a; Windows10Python 3.9.17opencv-python 4.8.0.74 二、二值化算法 2.1、概述 在机器视觉应用中&#xff0c;OpenCV的二值化函数threshold具有不可忽视的作用。主要的功能是将一幅灰度图进行二值化处理&#xff0c;以此大幅降低图像的数…

Linux:文件系统初步理解

文章目录 文件的初步理解C语言中对文件的接口系统调用的接口位图的理解open调用接口 文件和进程的关系进程和文件的低耦合 如何理解一切皆文件&#xff1f; 本篇总结的是关于Linux中文件的各种知识 文件的初步理解 在前面的文章中有两个观点&#xff0c;1. 文件 内容 属性&…

手撕A*算法(详解A*算法)

A*算法原理 全局路径规划算法&#xff0c;根据给定的起点和终点在全局地图上进行总体路径规划。 导航中使用A*算法计算出机器人到目标位置的最优路线&#xff0c;一般作为规划的参考路线 // 定义地图上的点 struct Point {int x,y; // 栅格行列Point(int x, int y):x(x),y(y){…