一个单机棋盘式半即时解谜RPG的开发与反思、1

本文详细介绍了游戏开发框架的设计思路、数据结构和 Lua 脚本的实现方式,包括包管理、地图编辑、游戏对象定义、动画支持以及 Json 数据格式的应用。同时阐述了 Lua 脚本的注册、回调机制和如何通过 Json 数据配置游戏对象的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

过年这几天写了几篇Angular2的学习笔记,算是有点“不务正业”,这次为大家带来我正在参与开发的一款游戏。这个小的系列大概有三、四篇组成,随着项目进展我会把这个坑填上。系列的内容是从程序的角度观察和反思这个游戏,包括它构建的基本思想和设计、编码过程中遇到的困难、重构来降低复杂度等。这个项目目前仅依赖于cocos2d,实际上他就是使用cocos2d来作为引擎的。需要说明的是,这个系列记述的是软件的开发过程和反思,不是一些结论性的东西来供读者参考,请不要把这些文章当做建议或者教程,如果你在阅读过程中有更好的简介和建议欢迎提出。

项目简介

这个游戏从程序的角度看(不包括设计、美术、音乐等),主要是两部分:cocos2d上面建立的框架、Lua实现的脚本。游戏2d的,主要玩法是玩家在一个棋盘式(格子)的地图上从一个点到另一个点,地图上有各种地形、道具、陷阱和墙壁,玩家何以拾取道具、破坏陷阱、合成道具从而完成游戏。玩家可以进行一些动作如走、跑、匍匐等,玩家有血量、体力等属性,以后可能还会加入战斗系统。游戏是半即时的,例如随时间的流逝体力会上升(站立时)以及滚石之类的陷阱等。最后游戏会加入对steam的支持,以上架为目标。

设计

Json:

为了方便地图编辑、数值修改,游戏数据采用Json记录。
分析需求得出游戏涉及到的对象包括:

包:游戏按包管理和加载资源,玩家可以开发自己的包来扩展游戏,包与包之间包含扩依赖关系,例如玩家包依赖于系统包,那么玩家设计的地图即可使用系统包的地形而不用重新开发。

地图:游戏包括很多地图,地图组织了一个地图块的矩阵,每个块可能包含地形(terrain)、道具(tool)、陷阱(trap)、墙(wall)、角色(role)。

地形、道具、陷阱:他们分配在格子上,在需要的时候调用Lua脚本。

墙:在格子之间放置,具有方向性(A到B阻塞、B到A阻塞或者双向阻塞)。

角色:可以在格子间移动,具备一些数值。

上面提到的这些可以提供统一的Lua脚本支持、Json中的属性可以直接在Lua中获取,这样玩家可以自由定义属性并添加脚本。Json提供对cocos2d的动画的直接支持,在Json中可以定义动画,这样在Lua中可以直接更改显示的动画。
游戏包含两个Json文件:

packages.json :

{
    "default":
    {
        "Name":"default",
        "Dir":"default/",
        "DescriptionFile":"description.json"
    },
    "UnitTest":
    {
        "Name":"UnitTest",
        "Dir":"UnitTest/",
        "DescriptionFile":"description.json"
    }
}

package.json :

{
    "Roles":
    {
        "default-Roles-player1":
        {
            "Name":"default-roles-player1"
            ,
            "ScriptFile":"roles/player1/script/script.lua"
            ,
            "FunctionName":"default-roles-player1-update"
            ,
            "FunctionMask":1
            ,
            "Animations":
            [
                {
                    "Name":"default-roles-player1-animations-default"
                    ,
                    "PLIST_File":"roles/player1/animations/default/default.plist"
                    ,
                    "Frames":
                    [
                        "default-roles-player1-animations-default-frame1.png"
                        ,
                        ...
                        ,
                        ...
                    ]
                    ,
                    "SpanTime":"0.33"
                    ,
                    "IsLoop":"true"
                }
                ,
                ...
                ,
                ...     
            ],
            "CurrentAnimation":"default-roles-player1-animations-default"
        }
    },
    "Tools":
    {
        "default-tools-knife":
        {
            "Name":"default-tools-knife"
            ,
            "ScriptFile":"tools/knife/script/script.lua"
            ,
            "FunctionName":"default-tools-knife-update"
            ,
            "Animations":
            [
                {
                    "Name":"default-tools-knife-animations-default"
                    ,
                    "PLIST_File":"tools/knife/animations/default/default.plist"
                    ,
                    "Frames":
                    [
                        "default-tools-knife-animations-default-frame1.png"
                        ,
                        ...
                        ,
                        ...
                    ]
                    ,
                    "SpanTime":"0.33"
                    ,
                    "IsLoop":"true"
                }
                ,
                ...
                ,
                ...     
            ],
            "CurrentAnimation":"default-tools-knife-animations-default"
        }
    }
    ,
    "Traps":
    {
        "default-traps-cirrus":
        {
            "Name":"default-traps-cirrus"
            ,
            "ScriptFile":"traps/cirrus/script/script.lua"
            ,
            "FunctionName":"default-traps-cirrus-update"
            ,
            "Animations":
            [
                {
                    "Name":"default-traps-cirrus-animations-default"
                    ,
                    "PLIST_File":"traps/cirrus/animations/default/default.plist"
                    ,
                    "Frames":
                    [
                        "default-traps-cirrus-animations-default-frame1.png"
                        ,
                        ...
                        ,
                        ...
                    ]
                    ,
                    "SpanTime":"0.33"                       ,
                    "IsLoop":"true"
                }
                ,
                ...
                ,
                ...     
            ],
            "CurrentAnimation":"default-traps-cirrus-animations-default"
        }
    }
    ,
    "Terrains":
    {
        "default-terrains-dirt":
        {
            "Name":"default-terrains-dirt"
            ,
            "ScriptFile":"terrains/dirt/script/script.lua"
            ,
            "FunctionName":"default-terrains-dirt-update"
            ,
            "Animations":
            [
                {
                    "Name":"default-terrains-dirt-animations-default"
                    ,
                    "PLIST_File":"terrains/dirt/animations/default/default.plist"
                    ,
                    "Frames":
                    [
                        "default-terrains-dirt-animations-default-frame1.png"
                        ,
                        ...
                        ,
                        ...
                    ]
                    ,
                    "SpanTime":"0.33"
                    ,
                    "IsLoop":"true"
                }
                ,
                ...
                ,
                ... 
            ],
            "CurrentAnimation":"default-terrains-dirt-animations-default"
        }
    }
    ,
    "Walls":
    {
        "default-walls-wall":
        {
            "Name":"default-walls-wall",
            ,
            "ScriptFile":"walls/wall/script/script.lua"
            ,
            "FunctionName":"default-walls-wall-update"
            ,
            "Animations":
            [
                {
                    "Name":"default-walls-wall-animations-default"
                    ,
                    "PLIST_File":"walls/wall/animations/default/default.plist"
                    ,
                    "Frames":
                    [
                        "default-walls-wall-animations-default-frame1.png"
                        ,
                        ...
                        ,
                        ...
                    ]
                    ,
                    "SpanTime":"0.33"
                    ,
                    "IsLoop":"true"
                }
                ,
                ...
                ,
                ... 
            ],
            "CurrentAnimation":"default-walls-wall-animations-default"
        }
    }
    "Maps":
    {
        "default-maps-map1":
        {
            "Name":"default-maps-map1"
            ,
            "Description":"this is the description of default-maps-map1"
            ,
            "Width":10
            ,
            "Height":10
            ,
            "TileWidth":56
            ,
            "TileHeight":56
            ,
            "ScriptFile":"maps/map1/script/script.lua"
            ,
            "FunctionName":"default-maps-map1-update"
            ,
            "Data":
            {
                "Tiles":
                [
                    [{"Terrain":{"Back":{"RefName":"default-terrains-dirt"},"Front":{"RefName":"default-terrains-ditr"}},"Trap":{"RefName":"default-item-traps-cirrus"},"Tool":{"RefName":"default-item-tools-knife"}},{},{},{},{},{},{},{},{},{}]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                    ,
                    [...]
                ],
                "Walls":
                [
                    {
                        "Wall":"default-walls-wall",
                        "Tile_X":2,
                        "Tile_Y":2,
                        "Direction":"RIGHT",
                        "Via":"FORWARD",
                        "IsRotate":false
                    }
                    ,
                    ...
                    ,
                    ...
                ],
                "Roles":
                [
                    {
                                    "RefName":"UnitTest-roles-player1",
                                    "Tile_X":2,
                                    "Tile_Y":2,
                                    "RoleType":"PLAYER",
                                    "ScriptFile":"roles/player1/script/script.lua",
                                    "FunctionName":"instanceUpdate",
                                    "FunctionMask":1
                                }
                    ,
                    ...
                    ,
                    ...
                ]
            }
        }
    }
}

对于packages.json很简单,包含了每个包的名称、目录及介绍,暂时没有涉及到包依赖,不过基本可以预见,以后包依赖的功能可以通过数组的方式简单的引入。

对于package.json,每一个包对应一个这样的json,包含了这个包的详细定义。
其中涉及到数据和实例的概念,与编程语言中的概念不同,后边我会介绍。首先是这个包的数据,包含Roles、Tools、Traps、Terrains、Walls和Maps。这些数据(集)是游戏的基础,其中Maps中定义了这个游戏中的实例,分别针对每个格子说明了它包含的内容以及所有墙壁和角色。
Roles、Tools、Terrains、Walls他们的内容相似,我以一个Role的定义为例:

{
    "Name":"default-roles-player1"
    ,
    "ScriptFile":"roles/player1/script/script.lua"
    ,
    "FunctionName":"default-roles-player1-update"
    ,
    "FunctionMask":1
    ,
    "Animations":
    [
        {
            "Name":"default-roles-player1-animations-default"
            ,
            "PLIST_File":"roles/player1/animations/default/default.plist"
            ,
            "Frames":
            [
                "default-roles-player1-animations-default-frame1.png"
                ,
                ...
                ,
                ...
            ]
            ,
            "SpanTime":"0.33"
            ,
            "IsLoop":"true"
        }
        ,
        ...
        ,
        ...     
    ],
    "CurrentAnimation":"default-roles-player1-animations-default"
}

Name定义了一个全局唯一的名字,ScriptFile定义了这个数据对应的脚本、FunctionName定义了脚本的回调函数名、FunctionMask定义了这个数据需要注册的Lua回调(会在Lua部分详述),Animations定义了这个数据的一系列动画,CurrentAnimation定义了当前使用的默认动画。
这里面关于Lua的部分(ScriptFile、FunctionName、FunctionMask)、Animation的部分(Animations)是通用的,凡是支持Lua和Animation的定义都应该使用这样的格式。
在Maps中定义了所有的地图,首先采用矩阵的方式来表示所有的块及其中所有的实例、通过列表表示的墙和角色,他们基本都可以通过Key的值很容易的理解其功能,我就不详细解释了。

关于数据和实例:

每一个地图上显示的物件都有一个数据存在,这个数据实例化许多实例。以小刀为例,有一个小刀的实例定义在Tools中,这定义了小刀的基本内容,在Map的矩阵中定义了小刀的实例,实例可能包括这个小刀的附魔、临时的属性等。这样可以设计许多具有不同特点的小刀,例如不同的贴图动画,在数据中声明所有的贴图,然后在实例中为不同的小刀做不同的标记,在Lua中通过标记来判断播放不同的动画。这些小刀又都具有数据中的相同特点,例如可以切断藤蔓。
这里所说的实例和实例化与编程语言(这里是C++)中的概念不同,请注意区分。可以这样说,小刀的数据实例化了一个数据类,小刀的实例实例化了一个实例类,这里的实例和实例化是语言层面的,进而小刀的实例实例化了小刀的数据,这里是游戏逻辑层面的。以后我用到的大部分“实例”和“实例化”是指游戏逻辑层面的。

Lua:

数据和实例可以绑定Lua脚本,在Json部分已经有定义,框架在加载Json时会执行指定的Lua脚本文件,将FunctionName指定的函数加载到Lua虚拟机中。框架采用事件调用的方式,在需要的时候调用这个函数,传入一个字符串标明是哪个事件。例如小刀数据的Lua文件为knife.lua,FunctionName为Update,框架在载入时会执行knife.lua,加载Update函数,以后所有关于小刀数据的回调都会使用这个Update函数。当在初始化时会调用这个Update并传入INIT标明这是在初始化时调用的。FunctionMask是一个二进制位标记的掩码,例如01b代表初始化,10b代表更新,11b代表同时注册初始化和更新。通过这个掩码框架会关闭不需要用到的回调,提高效率。
在Lua中可以通过一些全局函数来获取当前对象的属性,例如在lua代码中:

name = lua_get_string("name")

可以获取当前对象的属性,set怎可以设置属性,Json中的字段可以直接调用的Lua中。
框架会记录当前Lua调用所指向的对象,这个对象是静态的,因此无法提供对多线程的支持,这可能会在以后需要的时候优化。

框架设计:

框架包含三部分,这里不讨论cocos2d,仅仅是我实现的内容。
基础库、单元测试库、游戏主体

|      游戏主体
|——————————————————————
|  基础库 | 单元测试库
|——————————————————————
|     cocos2d-x

我曾经找过一个google的单元测试库,但似乎和cocos存在兼容问题,后来打算自己写一个,这个库不是主要任务,因为通过断言已经可以满足大部分需求。
基础库包括对游戏支持的通用组件,目前主要实现了单例类、Lua调用类、KV类、矩阵类、对象管理器等,我会把这个库开源出来和大家分享。
游戏主体是游戏的实现。

下一篇文章中我会着重对这些库进行介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值