Tutorials
Tutorials

This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English

跑酷游戏教程

在本教程中,我们从一个空项目开始,构建一个完整的跑酷游戏,包含动画角色、物理碰撞、收集品和计分系统。

学习新的游戏引擎时有很多内容需要消化,所以我们创建了这个教程来帮助您开始。这是一个相当完整的教程,详细介绍了引擎和编辑器的工作原理。我们假设您对编程有一定的了解。

如果您需要Lua编程入门,请查看我们的Lua in Defold手册

如果您觉得本教程有点难以入门,请查看我们的教程页面,其中有各种难度的教程选择。

如果您更喜欢观看视频教程,请查看YouTube上的视频版本

我们使用了来自其他两个教程的游戏资源,并进行了一些小的修改。教程分为几个步骤,每一部分都让我们朝着最终游戏迈出重要一步。

最终结果将是一个游戏,您控制一个英雄角色在环境中奔跑,收集硬币并避开障碍物。英雄角色以固定速度奔跑,玩家只能通过按下单个按钮(或在移动设备上触摸屏幕)来控制英雄角色的跳跃。关卡由无穷无尽的平台流组成,供玩家跳跃——以及要收集的硬币。

如果您在本教程或创建游戏时遇到困难,请不要犹豫,在Defold论坛上向我们寻求帮助。在论坛中,您可以讨论Defold,向Defold团队寻求帮助,了解其他游戏开发者如何解决他们的问题,并找到新的灵感。立即开始吧。

在整个教程中,对概念的详细描述以及如何执行某些操作的详细说明都标记为此段落。如果您觉得这些部分过于详细,请跳过它们。

那么让我们开始吧。我们希望您在完成本教程的过程中会有很多乐趣,并且它能帮助您开始使用Defold。

下载本教程的资源这里

STEP 1 - 安装与设置

第一步是下载以下文件

现在,如果您尚未下载并安装Defold编辑器,现在是时候这样做了:

下载

前往 Defold 下载页面,您会找到适用于 macOS、Windows 和 Linux (Ubuntu) 的下载按钮:

download editor

安装

在 macOS 上安装
下载的文件是一个包含程序的 DMG 镜像文件。
  1. 找到文件 “Defold-x86_64-macos.dmg” 并双击它以打开镜像。
  2. 将应用程序 “Defold” 拖到 “Applications” 文件夹链接中。

要启动编辑器,请打开您的 “Applications” 文件夹并双击文件 “Defold”。

Defold macOS

在 Windows 上安装
下载的文件是一个需要解压的 ZIP 压缩包:
  1. 找到压缩包文件 “Defold-x86_64-win32.zip”,按住(或右键单击)该文件夹,选择全部解压,然后按照说明将压缩包解压到一个名为 “Defold” 的文件夹中。
  2. 将文件夹 “Defold” 移动到您喜欢的位置(例如 D:\Defold)。您不应将 Defold 移动到 C:\Program Files (x86)\C:\Program Files\,因为这会阻止编辑器更新。

要启动编辑器,请打开文件夹 “Defold” 并双击文件 “Defold.exe”。

Defold windows

在 Linux 上安装
下载的文件是一个需要解压的 ZIP 压缩包:
  1. 从终端中,找到压缩包文件 “Defold-x86_64-linux.zip”,将其解压到一个名为 “Defold” 的目标目录。

    $ unzip Defold-x86_64-linux.zip -d Defold
    

要启动编辑器,请切换到您解压应用程序的目录,然后运行 Defold 可执行文件,或者在桌面上双击它。

$ cd Defold
$ ./Defold

Help > Create Desktop Entry 菜单中有一个安装桌面入口的助手。

如果您在启动编辑器、打开项目或运行 Defold 游戏时遇到任何问题,请参考 FAQ 的 Linux 部分

安装旧版本

Defold 的每个 beta 和稳定版本也在 GitHub 上可用

当编辑器安装并启动后,是时候创建一个新项目并准备就绪了。从”Empty Project”模板创建一个新项目

本教程使用Spine功能,该功能在Defold 1.2.188版本后已移至其自己的扩展。如果您使用的是较新版本,请将Spine扩展添加到game.project的依赖项部分。

编辑器

第一次启动编辑器时,编辑器是空白的,没有打开任何项目,所以从菜单中选择Open Project并选择您新创建的项目。系统还会提示您为项目创建一个”分支”。

现在,在Assets pane中,您将看到属于项目的所有文件。如果您双击”main/main.collection”文件,该文件将在中心的编辑器视图中打开:

编辑器概览

编辑器由以下主要区域组成:

Assets pane
这是项目中所有文件的视图。不同的文件类型有不同的图标。双击文件可在该文件类型的指定编辑器中打开。特殊的只读文件夹builtins是所有项目共有的,包括有用的项目,如默认渲染脚本、字体、用于渲染各种组件的材料等。
Main Editor View
根据您编辑的文件类型,此视图将显示该类型的编辑器。最常用的是您在此处看到的场景编辑器。每个打开的文件都显示在单独的选项卡中。
Changed Files
包含自上次同步以来您在分支中所做的所有编辑的列表。因此,如果您在此窗格中看到任何内容,则表示您有尚未在服务器上的更改。您可以通过此视图打开纯文本diff并还原更改。
Outline
当前编辑文件的分层视图中的内容。您可以通过此视图添加、删除、修改和选择对象和组件。
Properties
当前选定对象或组件上设置的属性。
Console
运行游戏时,此视图捕获来自游戏引擎的输出(日志、错误、调试信息等),以及脚本中的任何自定义print()pprint()调试消息。如果您的应用程序或游戏无法启动,控制台是首先要检查的地方。在控制台后面是一组显示错误信息的选项卡,以及一个用于构建粒子效果时使用的曲线编辑器。

运行游戏

“Empty”项目模板实际上是完全空的。尽管如此,请选择Project ▸ Build来构建项目并启动游戏。

构建

黑色屏幕可能不是很令人兴奋,但它是一个运行的Defold游戏应用程序,我们可以轻松地将其修改为更有趣的内容。那么让我们开始吧。

Defold编辑器处理文件。通过双击Assets pane中的文件,您可以在适合的编辑器中打开它。然后您可以使用该文件的内容。

当您完成文件编辑后,您必须保存它。在主菜单中选择File ▸ Save。编辑器通过在包含未保存更改的任何文件的选项卡中的文件名后面添加星号’*‘来提供提示。

带有未保存更改的文件

项目配置

在开始之前,让我们为项目设置几个设置。从Assets Pane中打开game.project资产,并向下滚动到Display部分。将项目的widthheight分别设置为1280720

您还需要将Spine扩展添加到项目中,以便我们可以为英雄角色制作动画。添加与您安装的Defold编辑器版本兼容的Spine扩展版本。可用的Spine版本可以在这里看到:

https://github.com/defold/extension-spine/releases

右键单击要使用的版本的zip文件的链接:

右键单击并复制发布链接

将发布链接添加到您的game.project dependencies列表中。添加Spine扩展后,您还需要重新启动编辑器以激活Spine扩展中包含的编辑器集成。

STEP 2 - 创建地面

让我们迈出第一步,为我们的角色创建一个竞技场,或者更确切地说是一块滚动的地面。我们分几步完成这个任务。

  1. 通过将”ground01.png”和”ground02.png”图像文件(来自资源包中的”level-images”子文件夹)拖到项目中的合适位置,例如”main”文件夹内的”images”文件夹,将图像资源导入项目中。
  2. 创建一个新的Atlas文件来保存地面纹理(在Assets pane中右键单击合适的文件夹,例如main文件夹,然后选择New ▸ Atlas File)。将图集文件命名为level.atlas

Atlas是一个将一组单独的图像合并为一个更大图像文件的文件。这样做的原因是为了节省空间并获得性能。您可以在2D graphics documentation中阅读更多关于Atlas和其他2D图形功能的信息。

  1. 通过右键单击Outline中的图集根目录并选择Add Images,将地面图像添加到新图集中。选择导入的图像,然后单击OK。图集中的每个图像现在可以作为单帧动画(静态图像)访问,用于精灵、粒子效果和其他视觉元素。保存文件。

创建新图集

将图像添加到图集

为什么它不起作用!人们在开始使用Defold时常遇到的一个问题是忘记保存!将图像添加到图集后,您需要保存文件才能访问该图像。

  1. 为地面创建一个集合文件ground.collection,并向其中添加7个游戏对象(在Outline视图中右键单击集合的根目录,然后选择Add Game Object)。通过在Properties视图中更改Id属性,将对象命名为”ground0”、”ground1”、”ground2”等。请注意,Defold会自动为新游戏对象分配唯一ID。

  2. 在每个对象中,添加一个精灵组件(在Outline视图中右键单击游戏对象,然后选择Add Component,然后选择Sprite并单击OK),将精灵组件的Image属性设置为您刚刚创建的图集,并将精灵的默认动画设置为两个地面图像之一。将_精灵组件_(不是游戏对象)的X位置设置为190,Y位置设置为40。由于图像的宽度为380像素,我们将其横向移动一半像素,游戏对象的枢轴将位于精灵图像的最左边缘。

创建地面集合

  1. 我们使用的图形有点太大,所以将每个游戏对象缩放到60%(X和Y缩放0.6,结果为228像素宽的地面片段)。

缩放地面

  1. 将所有_游戏对象_排成一行。将_游戏对象_(不是精灵组件)的X位置设置为0、228、456、684、912、1140和1368(宽度228像素的倍数)。

创建一个完整的带有精灵组件的缩放游戏对象然后复制可能是最简单的。在Outline视图中标记它,然后选择Edit ▸ Copy,然后选择Edit ▸ Paste

值得注意的是,如果您想要更大或更小的瓦片,您只需更改缩放比例即可。但是,这样做还需要您将所有地面游戏对象的X位置更改为新宽度的倍数。

  1. 保存文件,然后将ground.collection添加到main.collection文件中:首先双击main.collection文件,然后右键单击Outline视图中的根对象,然后选择Add Collection From File。在对话框中,选择ground.collection并单击OK。确保将ground.collection放置在位置0、0、0,否则它将在视觉上偏移。保存它。

  2. 启动游戏(Project ▸ Build)以查看一切是否就位。

静止的地面

到现在为止,您可能会感到困惑,想知道我们创建的所有这些东西到底是什么,所以让我们花点时间看看任何Defold项目中最基本的构建块:

游戏对象
这些是存在于运行游戏中的事物。每个游戏对象在3D空间中都有一个位置、旋转和缩放。它不一定是可见的。游戏对象持有任意数量的_组件_,这些组件添加了如图形(精灵、瓦片地图、模型、脊椎模型和粒子效果)、声音、物理、生成(用于生成)等能力。Lua_脚本组件_也可以被添加,以赋予游戏对象行为。游戏中存在的每个游戏对象都有一个id,您需要通过消息传递来与它通信。
集合
集合本身在运行游戏中不存在,但用于启用游戏对象的静态命名,同时允许同一游戏对象的多个实例。实际上,集合用作游戏对象和其他集合的容器。您可以像使用原型(也称为其他引擎中的”prefabs”或”blueprints”)一样使用集合,用于复杂的游戏对象和集合层次结构。在启动时,引擎加载一个主集合,并为您放入其中的任何内容注入生命。默认情况下,这是项目中main文件夹中的main.collection文件,但您可以在项目设置中更改它。

目前,这些描述可能已经足够了。但是,可以在Building blocks manual中找到对这些内容更全面的介绍。稍后访问该手册以获得对Defold工作原理的更深入理解是个好主意。

STEP 3 - 使地面移动

现在我们已经将所有地面片段放置到位,让它们移动起来相当简单。思路是这样的:将片段从右向左移动,当一个片段到达屏幕最左边缘时,将其移动到最右位置。移动所有这些游戏对象需要一个Lua脚本,所以让我们创建一个:

  1. 右键单击Assets pane中的main文件夹,然后选择New ▸ Script File。将新文件命名为ground.script
  2. 双击新文件以调出Lua脚本编辑器。
  3. 删除文件的默认内容,将以下Lua代码复制到其中,然后保存文件。
-- ground.script
local pieces = { "ground0", "ground1", "ground2", "ground3",
                    "ground4", "ground5", "ground6" } -- <1>

function init(self) -- <2>
    self.speed = 360  -- Speed in pixels/s
end

function update(self, dt) -- <3>
    for i, p in ipairs(pieces) do -- <4>
        local pos = go.get_position(p)
        if pos.x <= -228 then -- <5>
            pos.x = 1368 + (pos.x + 228)
        end
        pos.x = pos.x - self.speed * dt -- <6>
        go.set_position(pos, p) -- <7>
    end
end
  1. 将地面游戏对象的id存储在Lua表中,以便我们可以迭代它们。
  2. init()函数在游戏对象在游戏中诞生时被调用。我们启动一个对象本地成员变量,其中包含地面的速度。
  3. update()每帧调用一次,通常每秒60次。dt包含自上次调用以来的秒数。
  4. 遍历所有地面游戏对象。
  5. 将当前位置存储在局部变量中,然后如果当前对象在最左边缘,则将其移动到最右边缘。
  6. 以设定的速度减少当前X位置。乘以dt以获得以像素/秒为单位的帧率独立速度。
  7. 用新速度更新对象的位置。

Defold是一个快速引擎核心,管理您的数据和游戏对象。您需要的任何逻辑或行为都是用Lua语言创建的。Lua是一种快速轻量级的编程语言,非常适合编写游戏逻辑。有很多资源可以学习这门语言,比如书Programming in Lua和官方Lua reference manual

Defold在Lua之上添加了一组API,以及一个_消息传递_系统,允许您编程游戏对象之间的通信。有关其工作原理的详细信息,请参见Message passing manual

您可以使用键分别切换编辑器的Assets Pane、Console和Outline部分

现在我们有了一个脚本文件,我们应该在游戏对象的组件中添加对它的引用。这样,脚本将作为游戏对象生命周期的一部分执行。我们通过在ground.collection中创建一个新的游戏对象并向其中添加一个Script组件来引用我们刚刚创建的Lua脚本文件:

  1. 右键单击集合的根目录,然后选择Add Game Object。将对象的id设置为”controller”。
  2. 右键单击”controller”对象,然后选择Add Component from file,然后选择ground.script文件。

地面控制器

现在当您运行游戏时,”controller”游戏对象将在其Script组件中运行脚本,使地面在屏幕上平滑滚动。

STEP 4 - 创建英雄角色

英雄角色将是一个包含以下组件的游戏对象:

Spine Model
这给了我们一个像纸娃娃一样的小英雄角色,其身体部位可以平滑(且廉价)地动画化。
Collision Object
这将检测英雄角色与关卡中它可以运行、危险或可以拾取的事物之间的碰撞。
Script
这获取用户输入并对其做出反应,使英雄角色跳跃、动画和处理碰撞。

首先导入身体部位图像,然后将它们添加到我们称为hero.atlas的新图集中:

  1. 通过右键单击Assets pane并选择New ▸ Folder创建一个新文件夹。确保在单击之前不要选择文件夹,否则新文件夹将在标记的文件夹内创建。将文件夹命名为”hero”。
  2. 通过右键单击hero文件夹并选择New ▸ Atlas File创建一个新的图集文件。将文件命名为hero.atlas
  3. hero文件夹中创建一个新的子文件夹images。右键单击hero文件夹并选择New ▸ Folder
  4. 将身体部位图像从资源包中的hero-images文件夹拖到您在Assets pane中刚刚创建的images文件夹中。
  5. 打开hero.atlas,右键单击Outline中的根节点,然后选择Add Images。标记所有身体部位图像,然后单击OK
  6. 保存图集文件。

英雄图集

我们还需要导入Spine动画数据并为其设置Spine Scene

  1. 将文件hero.spinejson(包含在资源包中)拖到Assets pane中的hero文件夹中。
  2. 创建一个Spine Scene文件。右键单击hero文件夹并选择New ▸ Spine Scene File。将文件命名为hero.spinescene
  3. 双击新文件以打开和编辑Spine Scene
  4. spine_json属性设置为导入的JSON文件hero.spinejson。单击该属性,然后单击文件选择器按钮以打开资源浏览器。
  5. atlas属性设置为引用hero.atlas文件。
  6. 保存文件。

英雄脊椎场景

文件hero.spinejson已以Spine JSON格式导出。您需要Spine动画软件才能创建此类文件。如果您想使用其他动画软件,可以将动画导出为精灵表,并将其用作Tile SourceAtlas资源中的翻书动画。有关更多信息,请参见Animation手册。

构建游戏对象

现在我们可以开始构建英雄游戏对象:

  1. 创建一个新文件hero.go(右键单击hero文件夹并选择New ▸ Game Object File)。
  2. 打开游戏对象文件。
  3. 向其中添加一个Spine Model组件。(右键单击Outline中的根目录,然后选择Add Component,然后选择”Spine Model”。)
  4. 将组件的Spine Scene属性设置为您刚刚创建的文件hero.spinescene,并选择”run_right”作为默认动画(我们稍后会正确修复动画)
  5. 保存文件。

脊椎模型属性

现在是时候添加物理碰撞功能了:

  1. 向英雄游戏对象添加一个Collision Object组件。(右键单击Outline中的根目录,然后选择Add Component,然后选择”Collision Object”)
  2. 右键单击新组件,然后选择Add Shape。添加两个形状以覆盖角色的身体。一个球体和一个盒子就可以了。
  3. 单击形状,并使用Move ToolScene ▸ Move Tool)将形状移动到良好位置。
  4. 标记Collision Object组件,并将Type属性设置为”Kinematic”。

“Kinematic”碰撞意味着我们希望碰撞被注册,但物理引擎不会自动解决碰撞并模拟对象。物理引擎支持多种不同类型的碰撞对象。您可以在Physics documentation中阅读更多相关信息。

重要的是我们指定碰撞对象应该与什么交互:

  1. Group属性设置为名为”hero”的新碰撞组。
  2. Mask属性设置为另一个组”geometry”,此碰撞对象应该注册与该组的碰撞。请注意,”geometry”组尚不存在,但我们很快将添加属于该组的碰撞对象。

最后,创建一个新的hero.script文件并将其添加到游戏对象中。

  1. 右键单击Assets pane中的hero文件夹,然后选择New ▸ Script File。将新文件命名为hero.script
  2. 打开新文件,然后将以下代码复制并粘贴到脚本文件中,然后保存它。(代码相当简单,除了将英雄碰撞形状与其碰撞的对象分开的求解器。这是由handle_geometry_contact()函数完成的。)

英雄游戏对象

我们自己处理碰撞的原因是,如果我们将角色的碰撞对象类型设置为动态,引擎将对所涉及的物体执行牛顿模拟。对于这样的游戏,这样的模拟远非最佳,因此我们完全控制而不是与物理引擎的各种力量作斗争。

现在,要做到这一点并正确处理碰撞需要一点向量数学。在Physics documentation中给出了如何解决运动学碰撞的详细解释。

-- gravity pulling the player down in pixel units/sˆ2
local gravity = -20

-- take-off speed when jumping in pixel units/s
local jump_takeoff_speed = 900

function init(self)
    -- this tells the engine to send input to on_input() in this script
    msg.post(".", "acquire_input_focus")

    -- save the starting position
    self.position = go.get_position()

    -- keep track of movement vector and if there is ground contact
    self.velocity = vmath.vector3(0, 0, 0)
    self.ground_contact = false
end

function final(self)
    -- Return input focus when the object is deleted
    msg.post(".", "release_input_focus")
end

function update(self, dt)
    local gravity = vmath.vector3(0, gravity, 0)

    if not self.ground_contact then
        -- Apply gravity if there's no ground contact
        self.velocity = self.velocity + gravity
    end

    -- apply velocity to the player character
    go.set_position(go.get_position() + self.velocity * dt)

    -- reset volatile state
    self.correction = vmath.vector3()
    self.ground_contact = false
end

local function handle_geometry_contact(self, normal, distance)
    -- project the correction vector onto the contact normal
    -- (the correction vector is the 0-vector for the first contact point)
    local proj = vmath.dot(self.correction, normal)
    -- calculate the compensation we need to make for this contact point
    local comp = (distance - proj) * normal
    -- add it to the correction vector
    self.correction = self.correction + comp
    -- apply the compensation to the player character
    go.set_position(go.get_position() + comp)
    -- check if the normal points enough up to consider the player standing on the ground
    -- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
    if normal.y > 0.7 then
        self.ground_contact = true
    end
    -- project the velocity onto the normal
    proj = vmath.dot(self.velocity, normal)
    -- if the projection is negative, it means that some of the velocity points towards the contact point
    if proj < 0 then
        -- remove that component in that case
        self.velocity = self.velocity - proj * normal
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("contact_point_response") then
        -- check if we received a contact point message. One message for each contact point
        if message.group == hash("geometry") then
            handle_geometry_contact(self, message.normal, message.distance)
        end
    end
end

local function jump(self)
    -- only allow jump from ground
    if self.ground_contact then
        -- set take-off speed
        self.velocity.y = jump_takeoff_speed
    end
end

local function abort_jump(self)
    -- cut the jump short if we are still going up
    if self.velocity.y > 0 then
        -- scale down the upwards speed
        self.velocity.y = self.velocity.y * 0.5
    end
end

function on_input(self, action_id, action)
    if action_id == hash("jump") or action_id == hash("touch") then
        if action.pressed then
            jump(self)
        elseif action.released then
            abort_jump(self)
        end
    end
end
  1. 将脚本作为Script组件添加到英雄对象(右键单击hero.go中的Outline中的根目录,然后选择Add Component From File,然后选择hero.script文件)。

如果您愿意,现在可以尝试将英雄角色临时添加到主集合中并运行游戏,看看它穿过世界。

英雄功能所需的最后一件事是输入。上面的脚本已经包含了一个响应”jump”和”touch”(用于触摸屏)动作的on_input()函数。让我们为这些动作添加输入绑定。

  1. 打开”input/game.input_bindings”
  2. 为”KEY_SPACE”添加一个键触发器,并将动作命名为”jump”
  3. 为”TOUCH_MULTI”添加一个触摸触发器,并将动作命名为”touch”。(动作名称是任意的,但应与脚本中的名称匹配。请注意,您不能在多个触发器上使用相同的动作名称)
  4. 保存文件。

输入绑定

STEP 5 - 重构关卡

现在我们已经设置了一个带有碰撞和所有功能的英雄角色,我们还需要为地面添加碰撞,以便英雄角色有东西可以碰撞(或运行)。我们稍后会做这件事,但首先,我们应该做一点重构,将所有关卡内容放在一个单独的集合中,并稍微清理一下文件结构:

  1. 创建一个新的level.collection文件(右键单击Assets pane中的main,然后选择New ▸ Collection File)。
  2. 打开新文件,右键单击Outline中的根目录,然后选择Add Collection from File并选择ground.collection
  3. level.collection中,右键单击Outline中的根目录,然后选择Add Game Object File并选择hero.go
  4. 现在,在项目根目录中创建一个名为level的新文件夹(右键单击game.project下方的空白区域,然后选择New ▸ Folder),然后将您到目前为止创建的关卡资源移动到其中:文件level.collectionlevel.atlas、保存关卡图集图像的”images”文件夹,以及文件ground.collectionground.script
  5. 打开main.collection,删除ground.collection,而是添加level.collection(右键单击并选择Add Collection from File),现在包含ground.collection。确保将集合放置在位置0、0、0。

您可能已经注意到,Assets pane中看到的文件层次结构与您在集合中构建的内容结构是解耦的。单个文件从集合和游戏对象文件中引用,但它们的位置是完全任意的。

如果您想将文件移动到新位置,Defold会通过自动更新对文件的引用(重构)来帮助。当制作复杂的软件(如游戏)时,能够随着项目的增长和变化而改变项目结构是非常有帮助的。Defold鼓励这一点并使过程顺利进行,所以不要害怕移动您的文件!

我们还应该在关卡集合中添加一个带有脚本组件的控制器游戏对象:

  1. 创建一个新的脚本文件。右键单击Assets pane中的level文件夹,然后选择New ▸ Script File。将文件命名为controller.script
  2. 打开脚本文件,将以下代码复制到其中,然后保存文件:

     -- controller.script
     go.property("speed", 360) -- <1>
    
     function init(self)
         msg.post("ground/controller#ground", "set_speed", { speed = self.speed })
     end
    
    1. 这是一个脚本属性。我们将其设置为默认值,但脚本的任何放置实例都可以在编辑器的属性视图中覆盖此值。
  3. 打开level.collection文件。
  4. 右键单击Outline中的根目录,然后选择Add Game Object
  5. Id设置为”controller”。
  6. 右键单击Outline中的”controller”游戏对象,然后选择Add Component from File并选择level文件夹中的controller.script文件。
  7. 保存文件。

脚本属性

“controller”游戏对象不存在于文件中,而是在关卡集合中就地创建。这意味着游戏对象实例是从就地数据创建的。对于像这样的单一用途游戏对象,这很好。如果您需要多个相同游戏对象的实例,并且希望能够修改用于创建每个实例的原型/模板,只需创建一个游戏对象文件,并将游戏对象从文件添加到集合中。这将创建一个具有对文件作为原型/模板的引用的游戏对象。

现在,这个”controller”游戏对象的目的是控制与运行关卡相关的所有内容。很快,这个脚本将负责生成英雄角色与之交互的平台和硬币,但目前它只会设置关卡的速度。

在关卡控制器脚本的init()函数中,它向地面控制器对象的脚本组件发送一条消息,通过其id寻址:

msg.post("ground/controller#controller", "set_speed", { speed = self.speed })

控制器游戏对象的id设置为"ground/controller",因为它存在于”ground”集合中。然后我们在哈希字符"#"之后添加组件id"controller",该字符将对象id与组件id分开。请注意,地面脚本还没有任何代码来响应set_speed消息,所以我们必须向ground.script添加一个on_message()函数并为其添加逻辑。

  1. 打开ground.script
  2. 添加以下代码并保存文件:
-- ground.script
function on_message(self, message_id, message, sender)
    if message_id == hash("set_speed") then -- <1>
        self.speed = message.speed -- <2>
    end
end
  1. 所有消息在发送时都在内部进行哈希处理,必须与哈希值进行比较。
  2. 消息数据是一个包含与消息一起发送的数据的Lua表。

添加地面代码

STEP 6 - 地面物理与平台

此时我们应该为地面添加物理碰撞:

  1. 打开ground.collection文件。
  2. 向合适的游戏对象添加一个新的Collision Object组件。由于地面脚本不响应碰撞(所有逻辑都在英雄脚本中),我们可以将其放在任何_静止_游戏对象中(地面瓦片对象不是静止的,所以避免那些)。一个很好的候选者是”controller”游戏对象,但如果您愿意,可以为其创建一个单独的对象。右键单击游戏对象,然后选择Add Component并选择Collision Object
  3. 通过右键单击Collision Object组件并选择Add Shape然后选择Box来添加一个盒子形状。
  4. 使用Move ToolScale ToolScene ▸ Move ToolScene ▸ Scale Tool)使盒子覆盖所有地面瓦片。
  5. 将碰撞对象的Type属性设置为”Static”,因为地面物理不会移动。
  6. 将碰撞对象的Group属性设置为”geometry”,将Mask设置为”hero”。现在英雄的碰撞对象和这个对象将注册它们之间的碰撞。
  7. 保存文件。

地面碰撞

现在您应该能够尝试运行游戏(Project ▸ Build)。英雄角色应该在地面奔跑,并且应该能够使用Space按钮跳跃。如果您在移动设备上运行游戏,您可以通过点击屏幕来跳跃。

为了使我们的游戏世界生活不那么枯燥,我们应该添加可以跳跃的平台。

  1. 将图像文件rock_planks.png从资源包拖到level/images子文件夹。
  2. 打开level.atlas并将新图像添加到图集中(右键单击Outline中的根目录并选择Add Images)。
  3. 保存文件。
  4. level文件夹中创建一个新的Game Object文件,名为platform.go。(右键单击Assets pane中的level,然后选择New ▸ Game Object File
  5. 向游戏对象添加一个Sprite组件(右键单击Outline视图中的根目录,然后选择Add Component,然后选择Sprite)。
  6. Image属性设置为引用文件level.atlas,并将Default Animation设置为”rock_planks”。为方便起见,将关卡对象保存在子文件夹”level/objects”中。
  7. 向平台游戏对象添加一个Collision Object组件(右键单击Outline视图中的根目录,然后选择Add Component)。
  8. 确保将组件的Type设置为”Kinematic”,并将GroupMask分别设置为”geometry”和”hero”
  9. Collision Object组件添加一个Box Shape。(右键单击Collision Object组件,然后选择Add Shape,然后选择Box)。
  10. 使用Move ToolScale ToolScene ▸ Move ToolScene ▸ Scale Tool)使Collision Object组件中的形状覆盖平台。
  11. 创建一个Script文件platform.script(右键单击Assets pane,然后选择New ▸ Script File),并将以下代码放入文件中,然后保存它:

    -- platform.script
    function init(self)
        self.speed = 540      -- Default speed in pixels/s
    end
    
    function update(self, dt)
        local pos = go.get_position()
        if pos.x < -500 then
            go.delete() -- <1>
        end
        pos.x = pos.x - self.speed * dt
        go.set_position(pos)
    end
    
    function on_message(self, message_id, message, sender)
        if message_id == hash("set_speed") then
            self.speed = message.speed
        end
    end
    
    1. 当平台移动到屏幕右边缘外时只需删除它
  12. 打开platform.go并将新脚本添加为组件(右键单击Outline视图中的根目录,然后选择Add Component From File,然后选择platform.script)。
  13. platform.go复制到一个新文件(右键单击Assets pane中的文件,然后选择Copy,然后再次右键单击并选择Paste),并将新文件命名为platform_long.go
  14. 打开platform_long.go并添加第二个Sprite组件(右键单击Outline视图中的根目录,然后选择Add Component)。或者,您可以复制现有的Sprite
  15. 使用Move ToolScene ▸ Move Tool)将Sprite组件并排放置。
  16. 使用Move ToolScale Tool使Collision Object组件中的形状覆盖两个平台。

平台

请注意,platform.goplatform_long.go都有Script组件,它们引用同一个脚本文件。这是一件好事,因为我们对脚本文件所做的任何脚本更改都会影响常规和长平台的行为。

生成平台

游戏的想法是它应该是一个简单的无尽跑酷。这意味着平台游戏对象不能放置在编辑器的集合中。相反,我们必须动态生成它们:

  1. 打开level.collection
  2. 向”controller”游戏对象添加两个Factory组件(右键单击它,然后选择Add Component,然后选择Factory
  3. 将组件的Id属性设置为”platform_factory”和”platform_long_factory”。
  4. 将”platform_factory”的Prototype属性设置为/level/objects/platform.go文件。
  5. 将”platform_long_factory”的Prototype属性设置为/level/objects/platform_long.go文件。
  6. 保存文件。
  7. 打开管理关卡的controller.script文件。
  8. 修改脚本,使其包含以下内容,然后保存文件:
-- controller.script
go.property("speed", 360)

local grid = 460
local platform_heights = { 100, 200, 350 } -- <1>

function init(self)
    msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
    self.gridw = 0
end

function update(self, dt) -- <2>
    self.gridw = self.gridw + self.speed * dt

    if self.gridw >= grid then
        self.gridw = 0

        -- Maybe spawn a platform at random height
        if math.random() > 0.2 then
            local h = platform_heights[math.random(#platform_heights)]
            local f = "#platform_factory"
            if math.random() > 0.5 then
                f = "#platform_long_factory"
            end

            local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
            msg.post(p, "set_speed", { speed = self.speed })
        end
    end
end

1- 预定义的Y位置值,用于生成平台。 2- update()函数每帧调用一次,我们使用它来决定是否以特定间隔(避免重叠)和高度生成常规或长平台。很容易尝试各种生成算法来创建不同的游戏玩法。

现在运行游戏(Project ▸ Build)。

哇,这开始变成一个(几乎)可玩的东西了…

运行游戏

STEP 7 - 动画与死亡

我们要做的第一件事是为英雄角色注入生命。现在,可怜的东西被困在运行循环中,不能很好地响应跳跃或其他任何东西。我们从资源包中添加的Spine文件实际上包含了一组用于此目的的动画。

  1. 打开hero.script文件,并在现有的update()函数之前添加以下函数:
    -- hero.script
    local function play_animation(self, anim)
        -- only play animations which are not already playing
        if self.anim ~= anim then
            -- tell the spine model to play the animation
            local anim_props = { blend_duration = 0.15 }
            spine.play_anim("#spinemodel", anim, go.PLAYBACK_LOOP_FORWARD, anim_props)
            -- remember which animation is playing
            self.anim = anim
        end
    end

    local function update_animation(self)
        -- make sure the right animation is playing
        if self.ground_contact then
            play_animation(self, hash("run"))
        else
            play_animation(self, hash("jump"))

        end
    end
  1. 找到函数update()并添加对update_animation的调用:
    ...
    -- apply it to the player character
    go.set_position(go.get_position() + self.velocity * dt)

    update_animation(self)
    ...

插入英雄代码

Lua对局部变量具有”词法范围”,并且对您放置local函数的顺序很敏感。函数update()调用局部函数update_animation()play_animation(),这意味着运行时必须已经看到局部函数才能调用它。这就是为什么我们必须将函数放在update()之前。如果您切换函数的顺序,将会得到一个错误。请注意,这仅适用于local变量。您可以在http://www.lua.org/pil/6.2.html上阅读更多关于Lua的作用域规则和局部函数的信息

这就是为英雄添加跳跃和下落动画所需的全部内容。如果您运行游戏,您会注意到玩起来感觉好多了。您可能还会意识到,不幸的是,平台可以将英雄推离屏幕。这是碰撞处理的副作用,但补救措施很容易——添加暴力并使平台的边缘变得危险!

  1. spikes.png从资源包拖到Assets pane中的”level/images”文件夹。
  2. 打开level.atlas并添加图像(右键单击并选择Add Images
  3. 打开platform.go并添加几个Sprite组件。将Image设置为level.atlas,将Default Animation设置为”spikes”。
  4. 使用Move ToolRotate Tool将尖刺沿着平台的边缘放置。
  5. 为了使尖刺在平台后面渲染,将尖刺精灵的Z位置设置为-0.1。
  6. 向平台添加一个新的Collision Object组件(右键单击Outline中的根目录,然后选择Add Component)。将Group属性设置为”danger”。还将Mask设置为”hero”。
  7. Collision Object添加一个盒子形状(右键单击并选择Add Shape),并使用Move ToolScene ▸ Move Tool)和Scale Tool放置形状,以便英雄角色在从侧面或下方撞击平台时会与”danger”对象碰撞。
  8. 保存文件。

    平台尖刺

  9. 打开hero.go,标记Collision Object,并将”danger”名称添加到Mask属性中。然后保存文件。

    英雄碰撞

  10. 打开hero.script并更改on_message()函数,以便如果英雄角色与”danger”边缘碰撞,我们会得到反应:

    -- hero.script
    function on_message(self, message_id, message, sender)
        if message_id == hash("reset") then
            self.velocity = vmath.vector3(0, 0, 0)
            self.correction = vmath.vector3()
            self.ground_contact = false
            self.anim = nil
            go.set(".", "euler.z", 0)
            go.set_position(self.position)
            msg.post("#collisionobject", "enable")
    
        elseif message_id == hash("contact_point_response") then
            -- check if we received a contact point message
            if message.group == hash("danger") then
                -- Die and restart
                play_animation(self, hash("death"))
                msg.post("#collisionobject", "disable")
                -- <1>
                go.animate(".", "euler.z", go.PLAYBACK_ONCE_FORWARD, 160, go.EASING_LINEAR, 0.7)
                go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
                    function()
                        msg.post("#", "reset")
                    end)
            elseif message.group == hash("geometry") then
                handle_geometry_contact(self, message.normal, message.distance)
            end
        end
    end
    
    1. 在英雄死亡时添加旋转和下落移动。这可以大大改进!
  11. 更改init()函数以发送”reset”消息来初始化对象,然后保存文件:

    -- hero.script
    function init(self)
        -- this lets us handle input in this script
        msg.post(".", "acquire_input_focus")
        -- save position
        self.position = go.get_position()
        msg.post("#", "reset")
    end
    

STEP 8 - 重置关卡

如果您现在尝试游戏,很快就会明显发现重置机制不起作用。英雄重置没问题,但您可以轻松重置到会立即坠落到平台边缘并再次死亡的情况。我们想要做的是在死亡时正确重置整个关卡。由于关卡只是一系列生成的平台,我们只需要跟踪所有生成的平台,然后在重置时删除它们:

  1. 打开controller.script文件并编辑代码以存储所有生成的平台的id:

     -- controller.script
     go.property("speed", 360)
    
     local grid = 460
     local platform_heights = { 100, 200, 350 }
    
     function init(self)
         msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
         self.gridw = 0
         self.spawns = {} -- <1>
     end
    
     function update(self, dt)
         self.gridw = self.gridw + self.speed * dt
    
         if self.gridw >= grid then
             self.gridw = 0
    
             -- Maybe spawn a platform at random height
             if math.random() > 0.2 then
                 local h = platform_heights[math.random(#platform_heights)]
                 local f = "#platform_factory"
                 if math.random() > 0.5 then
                     f = "#platform_long_factory"
                 end
    
                 local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
                 msg.post(p, "set_speed", { speed = self.speed })
                 table.insert(self.spawns, p) -- <1>
             end
         end
     end
    
     function on_message(self, message_id, message, sender)
         if message_id == hash("reset") then -- <2>
             -- Tell the hero to reset.
             msg.post("hero#hero", "reset")
             -- Delete all platforms
             for i,p in ipairs(self.spawns) do
                 go.delete(p)
             end
             self.spawns = {}
         elseif message_id == hash("delete_spawn") then -- <3>
             for i,p in ipairs(self.spawns) do
                 if p == message.id then
                     table.remove(self.spawns, i)
                     go.delete(p)
                 end
             end
         end
     end
    
    1. 我们使用一个表来存储所有生成的平台
    2. “reset”消息删除表中存储的所有平台
    3. “delete_spawn”消息删除特定平台并将其从表中移除
  2. 保存文件。
  3. 打开platform.script并修改它,以便当平台到达最左边缘时,不仅删除平台,还向关卡控制器发送一条消息要求删除平台:

     -- platform.script
     ...
     if pos.x < -500 then
         msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
     end
     ...
    

    插入平台代码

  4. 保存文件。
  5. 打开hero.script。现在,我们需要做的最后一件事是告诉关卡进行重置。我们已经将要求英雄重置的消息移动到关卡控制器脚本中。像这样集中控制重置是有意义的,因为它允许我们,例如,引入更长的定时死亡序列,更容易实现:
-- hero.script
...
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
    function()
        msg.post("controller#controller", "reset")
    end)
...

插入英雄代码

现在主要的重启-死亡循环已经就位!

接下来 - 活着的目标:硬币!

STEP 9 - 收集金币

想法是在关卡中放置硬币供玩家收集。首先要问的是如何将它们放入关卡。例如,我们可以开发一个与平台生成算法某种程度上协调的生成方案。然而,我们最终选择了一个更简单的方法,只是让平台本身生成硬币:

  1. coin.png图像从资源包拖到Assets pane中的”level/images”。
  2. 打开level.atlas并添加图像(右键单击并选择Add Images)。
  3. level文件夹中创建一个Game Object文件,命名为coin.go(右键单击Assets pane中的level,然后选择New ▸ Game Object File)。
  4. 打开coin.go并添加一个Sprite组件(在Outline中右键单击并选择Add Component)。将Image设置为level.atlas,将Default Animation设置为”coin”。
  5. 添加一个Collision Object(在Outline中右键单击并选择Add Component) 并添加一个覆盖图像的Sphere形状(右键单击组件并选择Add Shape)。
  6. 使用Move ToolScene ▸ Move Tool)和Scale Tool使球体覆盖硬币图像。
  7. 将碰撞对象Type设置为”Kinematic”,将其Group设置为”pickup”,将其Mask设置为”hero”。
  8. 打开hero.go并将”pickup”添加到Collision Object组件的Mask属性中,然后保存文件。
  9. 创建一个新的脚本文件coin.script(右键单击Assets pane中的level,然后选择New ▸ Script File)。用以下内容替换模板代码:

     -- coin.script
     function init(self)
         self.collected = false
     end
    
     function on_message(self, message_id, message, sender)
         if self.collected == false and message_id == hash("collision_response") then
             self.collected = true
             msg.post("#sprite", "disable")
         elseif message_id == hash("start_animation") then
             pos = go.get_position()
             go.animate(go.get_id(), "position.y", go.PLAYBACK_LOOP_PINGPONG, pos.y + 24, go.EASING_INOUTSINE, 0.75, message.delay)
         end
     end
    
  10. 将脚本文件作为Script组件添加到硬币对象(在Outline中右键单击根目录,然后选择Add Component from File)。

    硬币游戏对象

计划是从平台对象生成硬币,所以在platform.goplatform_long.go中放入硬币的工厂。

  1. 打开platform.go并添加一个Factory组件(在Outline中右键单击并选择Add Component)。
  2. FactoryId设置为”coin_factory”,并将其Prototype设置为文件coin.go
  3. 现在打开platform_long.go并创建一个相同的Factory组件。
  4. 保存这两个文件。

硬币工厂

现在我们需要修改platform.script,使其生成和删除硬币:

-- platform.script
function init(self)
    self.speed = 540     -- Default speed in pixels/s
    self.coins = {}
end

function final(self)
    for i,p in ipairs(self.coins) do
        go.delete(p)
    end
end

function update(self, dt)
    local pos = go.get_position()
    if pos.x < -500 then
        msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
    end
    pos.x = pos.x - self.speed * dt
    go.set_position(pos)
end

function create_coins(self, params)
    local spacing = 56
    local pos = go.get_position()
    local x = pos.x - params.coins * (spacing*0.5) - 24
    for i = 1, params.coins do
        local coin = factory.create("#coin_factory", vmath.vector3(x + i * spacing , pos.y + 64, 1))
        msg.post(coin, "set_parent", { parent_id = go.get_id() }) -- <1>
        msg.post(coin, "start_animation", { delay = i/10 }) -- <2>
        table.insert(self.coins, coin)
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("set_speed") then
        self.speed = message.speed
    elseif message_id == hash("create_coins") then
        create_coins(self, message)
    end
end
  1. 通过将生成的硬币的父级设置为平台,它将随平台一起移动。
  2. 动画使硬币上下跳舞,相对于现在是硬币父级的平台。

父子关系严格来说是_场景图_的修改。子对象将与其父对象一起变换(移动、缩放或旋转)。如果您需要游戏对象之间的其他”所有权”关系,您需要在代码中专门跟踪它。

本教程的最后一步是向controller.script添加几行代码:

-- controller.script
...
local platform_heights = { 100, 200, 350 }
local coins = 3 -- <1>
...
  1. 在常规平台上生成的硬币数量。
-- controller.script
...
local coins = coins
if math.random() > 0.5 then
    f = "#platform_long_factory"
    coins = coins * 2 -- 长平台上的硬币数量是两倍
end
...
-- controller.script
...
msg.post(p, "set_speed", { speed = self.speed })
msg.post(p, "create_coins", { coins = coins })
table.insert(self.spawns, p)
...

插入控制器代码

现在我们有了一个简单但功能齐全的游戏!如果您能做到这一点,您可能想继续自己添加以下内容:

  1. 计分和生命计数器
  2. 拾取和死亡的粒子效果
  3. 漂亮的背景图像

此处下载项目的完整版本

这就结束了这个入门教程。现在继续深入Defold。我们准备了很多手册和教程来指导您,如果您遇到困难,欢迎来到论坛

祝您Defold愉快!


Did you spot an error or do you have a suggestion? Please let us know on GitHub!

GITHUB