Golang修改已编译程序的方法探讨

Golang修改已编译程序的方法探讨 我构思了一款使用Go和Raylib开发的游戏,其核心特色是高度支持模组修改。除了模组部分,其他大部分内容我都已经规划好了。我希望游戏本身是编译好的,模组也是编译好的。理想情况下,我构建好游戏后,游戏能够自动检测特定文件夹中的二进制文件。

模组主要会是新游戏对象的映射(即结构体的映射,结构体包含数据成员和需要定义的函数成员)。那么,我该如何实现这一点,以便在添加模组(或未来任何人添加模组)时,无需重新编译我的项目呢?更具体地说,我想实现类似《我的世界》模组那样的机制。你有一个存放jar文件(在我的案例中是二进制文件)的文件夹,《我的世界》的模组加载器会检测并加载它们。


更多关于Golang修改已编译程序的方法探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

我曾考虑过使用 JavaScript 或 Go 解释器作为备选方案,但前提是我的原始想法无法实现(或实现起来极其困难)。

更多关于Golang修改已编译程序的方法探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


通过研究Yaegi,它看起来确实非常出色。我认为我会使用Yaegi。使用二进制文件会让我和模组制作者都感到更加困难。

@bosko2

我立刻想到的有:

  • 使用 Lua 或 JavaScript 这类嵌入式脚本语言。有一些现成的包可以使用(可以在 GitHub 上找找)。
  • 使用像 Yaegi 这样的 Go 解释器。

要实现Go编译后程序的动态模组加载,核心是使用Go的插件系统(plugin package)。以下是具体实现方案:

1. 定义模组接口(主程序)

// mod_interface.go
package main

import "github.com/gen2brain/raylib-go/raylib"

type GameObject interface {
    Update()
    Draw()
    GetPosition() rl.Vector2
    SetPosition(rl.Vector2)
}

type ModInfo struct {
    Name    string
    Version string
    Author  string
}

type ModInitializer interface {
    Initialize() (ModInfo, []GameObject)
}

2. 主程序加载模组

// main.go
package main

import (
    "fmt"
    "plugin"
    "path/filepath"
    "os"
)

var activeMods []ModInfo
var gameObjects []GameObject

func loadMods(modDir string) {
    files, err := os.ReadDir(modDir)
    if err != nil {
        rl.TraceLog(rl.LogWarning, "无法读取模组目录")
        return
    }

    for _, file := range files {
        if filepath.Ext(file.Name()) == ".so" {
            modPath := filepath.Join(modDir, file.Name())
            p, err := plugin.Open(modPath)
            if err != nil {
                rl.TraceLog(rl.LogError, fmt.Sprintf("加载模组失败: %v", err))
                continue
            }

            initSym, err := p.Lookup("InitMod")
            if err != nil {
                rl.TraceLog(rl.LogError, "模组缺少InitMod函数")
                continue
            }

            initFunc, ok := initSym.(func() (ModInfo, []GameObject))
            if !ok {
                rl.TraceLog(rl.LogError, "InitMod签名不匹配")
                continue
            }

            modInfo, objects := initFunc()
            activeMods = append(activeMods, modInfo)
            gameObjects = append(gameObjects, objects...)
            
            rl.TraceLog(rl.LogInfo, fmt.Sprintf("加载模组: %s v%s", modInfo.Name, modInfo.Version))
        }
    }
}

func main() {
    rl.InitWindow(800, 600, "游戏模组示例")
    defer rl.CloseWindow()

    // 加载模组
    loadMods("./mods")

    for !rl.WindowShouldClose() {
        // 更新所有游戏对象
        for _, obj := range gameObjects {
            obj.Update()
        }

        rl.BeginDrawing()
        rl.ClearBackground(rl.RayWhite)
        
        // 绘制所有游戏对象
        for _, obj := range gameObjects {
            obj.Draw()
        }
        
        rl.EndDrawing()
    }
}

3. 模组实现示例

// mod_example.go
package main

import "github.com/gen2brain/raylib-go/raylib"

// 导出函数必须大写开头
func InitMod() (ModInfo, []GameObject) {
    info := ModInfo{
        Name:    "怪物模组",
        Version: "1.0.0",
        Author:  "模组作者",
    }

    // 创建自定义游戏对象
    monster := &Monster{
        position: rl.NewVector2(100, 100),
        texture:  rl.LoadTexture("monster.png"),
    }

    return info, []GameObject{monster}
}

// 自定义怪物类型
type Monster struct {
    position rl.Vector2
    texture  rl.Texture2D
}

func (m *Monster) Update() {
    // 怪物逻辑
    m.position.X += 1
}

func (m *Monster) Draw() {
    rl.DrawTexture(m.texture, int32(m.position.X), int32(m.position.Y), rl.White)
}

func (m *Monster) GetPosition() rl.Vector2 {
    return m.position
}

func (m *Monster) SetPosition(pos rl.Vector2) {
    m.position = pos
}

4. 编译模组

# 编译模组为共享库
go build -buildmode=plugin -o mods/monster_mod.so mod_example.go

# 编译主程序
go build -o game main.go mod_interface.go

5. 目录结构

game/
├── game                 # 主程序
├── mods/
│   ├── monster_mod.so  # 编译好的模组
│   └── weapon_mod.so   # 另一个模组
├── main.go
├── mod_interface.go
└── assets/             # 资源文件

6. 高级特性:热重载支持

// hot_reload.go
package main

import (
    "time"
    "os"
    "io"
    "crypto/md5"
)

type ModFile struct {
    Path    string
    ModTime time.Time
    Hash    [16]byte
}

func watchMods(modDir string, reloadChan chan<- string) {
    modFiles := make(map[string]ModFile)
    
    for {
        files, _ := os.ReadDir(modDir)
        for _, file := range files {
            if filepath.Ext(file.Name()) == ".so" {
                path := filepath.Join(modDir, file.Name())
                info, _ := os.Stat(path)
                
                // 计算文件哈希
                f, _ := os.Open(path)
                data, _ := io.ReadAll(f)
                f.Close()
                hash := md5.Sum(data)
                
                // 检查文件是否变化
                if old, exists := modFiles[path]; exists {
                    if old.ModTime != info.ModTime() || old.Hash != hash {
                        reloadChan <- path
                    }
                }
                
                modFiles[path] = ModFile{
                    Path:    path,
                    ModTime: info.ModTime(),
                    Hash:    hash,
                }
            }
        }
        time.Sleep(2 * time.Second)
    }
}

注意事项

  1. 平台限制:Go插件仅支持Linux、macOS和FreeBSD,不支持Windows
  2. 版本兼容:主程序和模组必须使用相同Go版本编译
  3. 依赖管理:模组和主程序需要共享相同的接口定义
  4. 内存安全:插件卸载可能导致内存泄漏,建议保持插件常驻

此方案通过Go的plugin包实现了编译后程序的动态模组加载,模组可以独立编译为.so文件,主程序运行时自动检测并加载。

回到顶部