内容


用 Rational Application Developer 创建 Second Life(第二人生)脚本

创建 Second Life 中的迷你游戏

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分:

敬请期待该系列的后续内容。

此内容是该系列的一部分:

敬请期待该系列的后续内容。

预备知识

本教程面向那些希望使用他们喜爱的开发工具集在 Second Life 网格上创建新内容的 Rational® Application Developer 用户。您将安装 Rational Application Developer,使用 Linden Script Language(LSL),然后开发一个 Second Life 世界中的小游戏。

关于本教程

随着 Meridian 59 的发布,从 1996 年起,大规模多人在线游戏(Massively Multiplayer Online game,MMO)开始发展起来,直到现在,每个产品要么是定位于宇宙,要么是幻想的陆地上,所有的都属于标准的角色扮演经营的范围内。但是,2003 年,Second Life 的发布改变了这种情况。该游戏提出了罕见但大胆的假设:令用户在从未涉足的领域中面临了创造性的挑战,该游戏让用户构建或创造该游戏可能表现的任何东西。

Second Life 为游戏界面本身提供了一组游戏编辑工具,并且以 Linden Script Language(LSL)的形式提供丰富的、事件驱动的脚本。为了辅助开发,您可以利用 Rational Application Developer,一种基于 Eclipse 的 IDE。为了示范 Second Life 中的一些特性,以及 Rational 如何提供帮助,您可以开发一个简单的游戏,一种 Sumo、Go,和 Marble 的混合。该游戏将装配在一个物品,图腾中,玩家的化身可以放在储备中携带,或者进行使用。他们可以使用该游戏图腾启动新的游戏。

先决条件

要求基本了解某种语言的程序设计知识。了解事件和向量代数一定有帮助,但不是必要的。

系统需求

要完成本教程中的步骤,您需要以下工具:

  • Rational Application Developer —— 如果您还没有,可以下载试用版本。后面将介绍安装它的更详细的信息。
  • Second Life。 —— 在您注册帐户之后可以免费使用。
  • ByronStar SL 插件 —— 为 SecondLife 提供的基于 eclipse 的集成开发环境。
  • Windows XP SP2 或 Windows 2000 SP4(不支持 Vista)。800Mhz、256MB,Geforce 2 或 ATI Radeon 8500 或更高。

安装

在您进行有趣的部分之前,让我们先进行开发环境的安装。注意:我在运行 Windows XP Pro 64 的 AMD Opteron 165 上执行本教程。

Second Life(第二人生)

首先,您应该下载并安装 Second Life 客户端(参见参考资料)。要求注册免费帐户,信用卡是可选择的。一旦安装了该游戏,启动并登录,确保您的机器可以安全地运行。最初我遇到了稳定性和跳帧问题,但当我安装了最新版的显卡驱动,并重新启动机器后,问题就解决了。

Rational Application Developer

如果您还没有使用过它,那么本教程将带您使用试用版(参见参考资料)。虽然,在大多数情况下,我避免使用安装管理器或下载管理器,但是,在此情况下,我强烈推荐使用 IBM 的 Installation Manager。Installation Manager 将会令您只下载您需要的特性(在此情况下,大约 150MB)。如果您没有 IBM ID,那么您必须创建一个,同意许可证,并填写一个关于您的试用目的的简短调查。一旦您通过了这些验证,就可以选择并下载 IBMIM_RAD_win32.exe 了。

一旦您启动了该可执行程序,它就将安装 Installation Manager,并且直接启动 Rational Application Developer 的安装。如果一会儿您需要重新启动此过程,那么可以从 Start Menu(Start > Programs > IBM Installation Manager)启动 Installation Manager。

安装过程的开始应该是 Install Packages 屏幕,其中选择了‘IBM Rational Application Developer’和‘Version 7.0.0’选项,因此您只需单击 Next。在您同意了许可证之后,为共享文件选择一个位置,并且为基本应用程序文件选择一个位置。安装程序会询问您是否想要扩展现有的 Eclipse,在此情况下,您不希望做这件事,因此,按照默认设置,不要选择。由于您不扩展现有的 Eclipse,并且对于本教程来说,只使用编辑器特性,所以您现在可以取消下一屏的所有选项。当您取消选择所有的选项之后(参见图 1),整个安装大小应该下降为大约 230MB。

图 1. 取消选择了 Rational Application Developer 安装过程中的不必要特性
取消选择了 Rational Application Developer 安装过程中的不必要特性
取消选择了 Rational Application Developer 安装过程中的不必要特性

当您单击 next,并且填写了简短的调查(如果您在使用试用版)之后,您将会看到启动最小的 Rational 安装的下载和配置的 Install 按钮。安装过程中不需要再输入,安装完成后将询问是否愿意立即启动 Rational。确保选择 Rational,并单击 Finish。

当您为默认的工作区选择了一个位置后,您应该看到一个新安装的 Rational Application Developer。

创建新的项目(File > New > Project > [+] General > Project)。并对其随意命名。

ByronStar SL

现在,是时候通过 ByronStar SL 插件安装 Rational 中的 Linden Script 支持了,如果您想要直接下载并/或构建,可以参见参考资料。但是,简单得多的方法是使用 Eclipse 更新特性,并且让其为您自动下载和安装插件。要添加更新网站,在 Help 菜单中,选择 Help > Software Updates > Find and Install。选择 Search for new features to install,然后选择 New Remote Site 按钮。随您的喜好进行命名,并使用 url:http://byronstar-sl.sourceforge.net/update(参见图 2)。

图 2. New Update Site
New Update Site
New Update Site

选择 OK,然后选择 Finish。开始执行对新版本的搜索。该搜索找到了最新的 ByronStar SL,要求您同意其许可证,然后为您自动地下载并安装。安装之后,重新启动 Rational。

一旦您重新启动,您创建的扩展名为 .lsl 的新文件都能按语法突出显示,并且拥有代码完成功能和 API 文档(参见图 3)。编辑器显示代码完成下拉框的热键默认为 Ctrl-Space。

图 3. ByronStar 的代码完成功能
ByronStar 的代码完成功能
ByronStar 的代码完成功能

在本教程中使用代码完成功能编码将帮助您更详细地了解 LSL,并且强烈推荐。

创建 Second Life 的脚本

在您开始使用 Rational 编写脚本之前,让我们来讨论一些您将使用的系统的基础知识。

图元

图元,或简称 Prims,定义了一个空间的几何区域,以及它包含的物质。您在 Second Life 中可以进行交互的每样东西都是由图元构成的。图元可以与其他图元链接在一起,并且结果的复合物体可以作为一个单位进行控制。每个图元总是有一个当前状态,而每个状态都有一组事件处理程序。

状态

世界中的每个物体总是处于某个状态。每个状态都由一个名称指定。如果您不提供额外的状态,那么物体将处于名为 default 的状态。

使用 LSL 为物体定义多个状态,并在状态间转换是可能的。

每个状态都包含一组具体到该状态的事件处理程序。

清单 1. 两个状态:default 和 started
default {
  state_entry () {
    llWhisper (0, "Entered default state.");
  }
  touch (integer total) {
    llWhisper (0, "Change to started state.");
    // state is the command that changes the current state!
    state started;
  }
}
state started {
  state_entry () {
    llWhisper (0, "Entered started state.");
  }
  touch (integer total) {
    llWhisper (0, "Changing back to default state.");
    state default;
  }
}

如果您将该脚本放入空的物体中 —— 您将会立刻看到如何做 —— 然后触摸它,每次触摸将使该物体的状态从 default 切换为 started,然后再回来。注意,每个状态如何向触摸事件提供其自己的处理程序。这是其中一个最有力的设计模式,而它正好在您的指尖,所以在可能的时候尝试利用它。

事件

事件由某种游戏中的活动生成。举例来说,当新状态为 entered 时,生成 state_entry 事件,当“游戏中”的玩家触摸物体时,生成 touch 事件,当其他物体撞击游戏中的什么东西时,生成 collision 事件,等等。

当生成事件时,事件就会在当前状态中寻找事件处理程序,并在该处理程序中执行代码。您执行的每一点代码都是事件处理程序。后面所讨论的较完整的代码实例中会有关于不同事件的更详细的信息。眼下,您在前面的实例中可以看到有两个事件在活动中:state_entrytouch

您的第一个脚本

如果您不是一个熟悉基于事件的架构的程序设计人员那么这可能有一点令人不解,因此,让我们来看一个实例。

教授 C 时的第一个标准的程序是 printf("Hello World");,同样,Second Life 也有典型的开始程序。

创建图元

  1. 创建世界中的 Prim。做到这件事要确保您处于允许构建的 Sand Box 中(如果您是新手,试一试 Help Island),然后右键单击地面,并从饼形菜单中选择 Create 如图 4 所示。
    图 4. 饼形菜单的 Create 层
    饼形菜单的 Create 层
    饼形菜单的 Create 层
  2. 选择您想要创建的 Prim 形状,然后左键单击世界中的识别笔。您应该看到世界中您点击的地方出现一个新的 Prim(参见图 5)。
    图 5. 创建一个新的物体
    创建一个新的物体
    创建一个新的物体
  3. 右键单击此 Prim 并选择 Edit
  4. 来到物体编辑器中的 Content 选项卡,并单击 New Script。如果您没有看到 Content 选项卡,那么就确保您点击 More 按钮来展开编辑器。一旦您点击了 New Script,脚本将自动填充默认的 Hello World 脚本(参见清单 2)。
    清单 2. 说 hello 的脚本
    default {
      state_entry () {
        llSay (0, "Hello World!");
      }
      touch_start (integer total_number) {
        llSay (0, "Touched!");
      }
    }

    图 6 中显示出在 Second Life 中执行的脚本。

    图 6. 默认的脚本
    默认的脚本
    默认的脚本

现在,让我们来讨论一下此脚本。每个脚本都是一组状态及其相应的事件处理程序的集合。在此脚本中,您只处理了一个状态: default

在该状态中,定义了两个事件处理程序来处理事件:touchstate_entrydefault 状态中的 state_entry 总是创建物体时第一个触发的事件。因此,您一创建世界中的该物体的实例,它就会宣告它的存在,而您将在对话面板中看到“Object: Hello World!”。类似的,如果您触摸该物体(通过单击右键,并选择 Touch),您将在对话面板中看到“Object: Touched!”。

活起来

现在您有了一个会回话的固定的物体,但它不会为您带来很多有趣的游戏。您需要能够让东西走来走去,并且以更有意义的方式与它们交互。因此,让我们展开上面的 touch 事件。想着您未来的迷你游戏,制作一个可以到处踢的球。

  1. 创建一个球形的新 Prim。(您可以右键单击第一个,并选择饼形菜单中的 MoreDelete 来删除。)您可以使用物体编辑器中的缩放工具,将球扩展到您觉得适合您的大小。
  2. 当您有了世界中地面上的一个球之后,来到物体编辑器中。在 Object 选项卡中,确保选择了 Physical。这意味着向您的物体应用物理惯例,它将落到地面上,撞到东西,其最重要的是,您可以从事件处理程序中向它施加力。
  3. 清单 3 显示出非常简短的实例,允许物体的所有者通过触摸物体,在世界中到处踢它(右键单击物体,并在饼形菜单中选择 Touch,或者如果您处于鼠标模式,您可以简单地左键单击该物体来进行触摸)。
    清单 3. 一个您可以踢的球
    default {
      state_entry () {
        llSetStatus (1, TRUE); // make sure that Physical is turned on
      }
    
      touch_start (integer total_number)
      {
        if( llDetectedKey(0) == llGetOwner() ) {
          llSay (0,"Being Kicked.");
          llApplyImpulse (<2, 0, 0, 0>, FALSE);
        }
      }
    }
  4. 在物体编辑器中的 Content 选项卡中,使用 New Script 将此脚本添加到您面前的地上的球中。

活动中的图元

现在,如果您右键单击物体,并选择 Touch,物体就好像受到沿 x 轴的大小为 1 的力打击一样,应该沿着 x 轴移动。这是 llApplyImpulse 方法的结果,它采用了力向量,并作为一次性力量将其应用于物体。

这额外的踢动作只允许由物体的所有者发出(也就是您)。该规则是由您自己的脚本中的代码行:if(llDetectedKey(0)==llGetOwner()) 执行的。后面将更详细地介绍 llDetected 方法。

碰撞检测

所有的碰撞检测都是由物理引擎处理的,当物体与其他任何东西碰撞时,就会简单地触发碰撞事件,collision_startcollision_end。要探索此问题,让我们来改装您的球,让它响应与玩家的碰撞,取代触摸事件,使其更像实际的踢动作(参见图 4)。

清单 4. 您可以用脚踢的球!
default {
  state_entry () {
    llSetStatus (1, TRUE); // make sure that Physical is turned on
  }
  collision_start (integer total_number) {
    if( llDetectedKey(0) == llGetOwner() ) {
    llSay (0, "Collided with "+llDetectedName (0));
    llApplyImpulse (<2, 0, 0>,> FALSE);
  }
}

现在,如果您用上面的代码覆盖了您的球的旧脚本,那么您就可以在地上踢它了,并且它会说‘Object: Collided with <Your character name>’并且沿着 x 轴蹦跳。

记住使用碰撞事件的结果是该事件可以用比触摸(要求玩家的有意识的动作)更少的控制来触发。一个游戏物体触碰到其他物体时就会出现碰撞,即使该游戏物体对您来说像个地板。如果不是真实的陆地,那么它就是一个游戏物体,并且会碰撞。如果像最近两个实例中一样,您不根据 llGetOwner 进行检查,那么您就会与地板不断碰撞,而您的球将会很快消失。

收集信息

这是一个探讨 LSL Library 中一组非常重要的方法的绝佳机会:llDetected* 方法。在任何将整数 total_number 作为参数的事件中, total_number 指的是同时发生该事件的数量,或者当前事件的参与人的数量。llDetected* 方法收集有关第索引号次发生当前事件时的信息。因此,在此实例中, llDetectedName(0) 获得了与当前物体第一个碰撞的人的名称。如果有两个人同时撞击,将会忽略第二个,因为您只参考索引 0。随后,当覆盖了传感器时,您将循环处理一组 llDetected* 响应。

同样,此时您的踢脚本的一个问题是,球总是沿着 x 轴移动。您如何使其沿着您踢的方向移动?此处 llDetected 方法的其中之一可以帮助您。llDetectedPos 返回一个向量,指示产生该事件的第索引号个代理的 3d 位置。

增加复杂性

这如何能够帮助您呢?您可以找到玩家踢球的位置,并且使用某个简单的线性代数来找到应该传递给 llApplyImpulse 的向量,取代总是传递 <1,0,0>

清单 5. 您可以踢的球,之后该球会离您远去
default {
  state_entry () {
    llSetStatus (1, TRUE); // make sure that Physical is turned on
  }
  collision_start (integer total_number) {
    llSay (0, "Collided with "+llDetectedName (0));
    vector agent_pos = llDetectedPos (0); // get the kicker's position
    // now get the vector that points from the kicker to the ball
    vector diff = llGetPos () - agent_pos;
    // since the agent is probably higher up, 
    // and we don't want to push down into the ground
    // force the z coordinate up
    diff.z = .1;
    // scale the vector to a fixed length
    // otherwise, the kick is always as strong as the distance
    // between the agent and the ball
    vector kick_vector = llVecNorm (diff)*2;
    // now apply the kick vector to the ball and watch it go
    llApplyImpulse (kick_vector, FALSE);
  }
}

传感器

LSL 系统中的另一个关键组件是传感器。

将传感器认为是在计时器上一次或重复触发的声纳脉冲信号。每次传感器扫描完毕时,将触发一个 sensor 事件。因此,使用传感器的基本方法是定义初始的扫描参数,然后提供循环扫描一次中找到的所有物体的传感器事件处理程序。现在又是一个示范如何循环一系列对 llDetected* 方法的调用的绝佳机会(参见清单 6)。

清单 6. 打印出 10 米内所有人的名称
default {
  state_entry () {
    // set up a recurring sensor that looks for AGENTs, 
    // within 10.0 meters, in a complete circle, every 1.0 seconds
    llSensorRepeat ("","", AGENT, 10.0, 3.1415926, 1.0);
  }
  sensor (integer total) { 
    integer i;
    for (i = 0; i < total; i ++) {
      string name = llDetectedName (i);
      llWhisper (0, name+" is within 10 meters");
    }
  }
}

该实例打印出离物体 10 米内每个玩家化身的名字。它用参数 total 控制对 llDetectedName 调用的循环,以确保您不会错过任何人(参见图 7)。

图 7. 传感器
传感器
传感器

少做

您可以使用 llSensor 来设立传感器的单独扫描,在不重复的情况下触发同一个传感器事件。您同时只可以有一个传感器事件处理程序,因此,您不能让一个循环的传感器运行,然后发出单独扫描。您不得不调用 You have to stop one by calling llSensorRemove 来停止一个,然后在重新启动您的循环扫描之前扫描并处理它(参见清单 7)。

清单 7. 触发单独的传感器扫描
default {
  touch(integer total_number) {
  	llSensor("","",AGENT,20, 3.1415926);
  }
  sensor(integer total_number) {
  	llWhisper(0, "I see "+llDetectedName(0));
  }
}

这只报告一次,然后该传感器不再重复。

记住重复的传感器和单独的传感器都触发同样的事件:sensor。由于每个状态的每个事件都只有一个处理程序,所以一个事件处理程序将收到所有的传感器事件。生成的事件是相同的,但是通过自己设置标志,您的代码应该知道目前运行的是否是重复的传感器,或者它是否在处理您的单独的脉冲。虽然做到这些是可行的,但是这是笨拙的设计模式,因此我不会详细地介绍一个实例。较好的方法是在单独的状态中设计,以便您可以拥有单独的传感器事件处理程序,每个状态一个(参见清单 8)。

清单 8. 混合的传感器类型
default {
  state_entry() {
    state single;
  }	
}

state repeat {
  state_entry() {
    // start a repeating sensor
    llSensorRepeat("","",AGENT,20,3.1415926, 1.0);
  }
  sensor(integer total) {
    llSay(0, "Repeating: I see "+(string)total+" agents.");
  }
  state_exit() {
    // turn off the repeating sensor
    llSensorRemove();
  }
  touch(integer total) {
    state single;
  }
}
		
state single {
  state_entry() {
    // fire a sensor once
    llSensor("","",AGENT,20,3.1415926);
  }
  sensor(integer total) {
    llSay(0, "Once: I see "+(string)total+" agents.");
  }
  state_exit() {
    // clean up the single sensor
    // even though it isn't repeating, 
    // we might leave this state before the sensor fires once
    llSensorRemove();
  }
  touch(integer total) {
    state repeat;
  }
}

此处您可以看到使用两个状态的清晰设计,重复的状态和单独的状态。物体将在单独的状态中开始,只触发一个传感器。触摸物体将在重复和单独之间切换,您可以看到两者的区别。

沟通

您可能想让世界中的物体直接与您或其他物体进行沟通。您可以找到大量沟通方法,要示范这些方法,您可以构建一个将所听到文字转播给可以在第二个位置说出文字的伙伴的小型代理物体,类似于电话会议装置。

所有的沟通都基于信道,listen 事件。信道只是和每个消息一起发送的整数标识符。您签署使用特定的信道,通过调用 llListen 开始接收 listen 事件。要展示一个实例,让我们来看看一个小的脚本,它可以监听,并且处理通过私用对话信道给出的命令(参见清单 9)。

清单 9. 可以根据文字变更颜色的物体
default {
  state_entry () {
    llListen (0, "", "","");
  }
  listen (integer channel, string name, key id, string message) {
    if (message == "blue") {
      llSetColor (<0, 0, 255>, ALL_SIDES);
    } else if (message == "red") {
      llSetColor (<255, 0, 0>, ALL_SIDES);
    }
  }
}

llListen 的第一次调用登记了该物体,在新消息到达信道 0 时接收 listen 事件,不对来源或内容进行过滤。现在,如果您将该脚本放在您附近的物体中,然后说单词‘blue’,那么它将变为蓝色,说‘red’,它将变为红色(参见图 8)。

图 8. 对物体说话
对物体说话
对物体说话

告诫

该实例正在监听公共信道 0 中的真实对话。您不想这样做。 llListen 的最有趣的用法是使用物体之间发送信号的私用信道。任何 0 以上的信道都是玩家和化身不能监听或在对话时使用的信道,但是物体可以使用信道 0 来进行数据交换。您将在后面的迷你游戏中看到这样的用法。

动画和玩家化身

物体和脚本与玩家之间的交互有三种主要方式。

  1. 玩家可以通过“坐”在物体上来链接物体。为物体给出定制此过程流的事件和控制,以便您可以让玩家化身坐在,例如复合的交通工具的正确位置上。
  2. 玩家可以‘穿’物体。要完成这件事,物体必须附着于身体的许多绑定点上(要查看列表,右键单击您的储备中的物体,并选择 Attach。您将获得一个身体可绑定点的完整列表)。
  3. 具有足够权限的物体可以请求玩家化身启动或停止运行着的某个指定动画例程。

坐下

第一个方法,坐在物体上,是由方法 llSitTarget 和 changed 事件处理的(参见清单 10)。

清单 10. 使用坐目标
default {
  state_entry() {
    llSitTarget(<0.0, 0.0, 0.1>, ZERO_ROTATION);
  }
  changed(integer change) {
    if (change & CHANGED_LINK) {
      llWhisper(0, llKey2Name(llAvatarOnSitTarget())+" sat down.");
    }
  }
}

不论什么时候有人坐下,都会‘链接’到所坐的物体。该动作触发了带有包括 CHANGED_LINK 参数的 changed 事件,指示有人链接到该物体。在坐完成之前,请求坐的玩家化身被传递给 llSitTarget 的参数平移并旋转。在上面实例的情况下,它们被提升了 .1 米,没有旋转。llAvatarOnSitTarget 返回现在坐着的玩家化身的键。

记住您总是根据物体的局部轴坐下。因此,如果您坐在旋转的球上,您也将旋转,不管您传递给 llSitTarget 的旋转参数是什么。

附着(穿)

将脚本化的物体附着到您的身体上是超级简单的事,并且需要非常少的代码。但它迫使我们引入一个新概念:权限(permissions)。为了附着到人的身体上,您必须申请权限标记 PERMISSION_ATTACH。用 llGetPermissions 来核实,您是否已经拥有,如果没有,使用 llRequestPermissions 打开对话框,向用户请求权限。

llRequestPermissions 从用户那里得到回答之后,用新的权限触发事件 run_time_permissions。注意:它不返回。这是无阻塞的调用。

只有在触发该事件,并且您已经复核您现在拥有适当的权限之后,就可以安全调用 llAttachToAvatar(参见清单 11)。

清单 11. 一生成就自动附着到所有者手腕上的物体
default {
  state_entry() {
    llRequestPermissions(llGetOwner(), PERMISSION_ATTACH);
  }
  run_time_permissions(integer perm) {
    if (perm & PERMISSION_ATTACH)  {
      llAttachToAvatar(ATTACH_RLARM);
    }
  }
}

图 9 显示出请求附着到玩家化身的权限的物体。

图 9. 获得权限
获得权限
获得权限

制作动画

为了让玩家化身动起来,您必须拥有权限 PERMISSION_TRIGGER_ANIMATION。像前面实例中一样进行,并请求权限,一旦您得到允许就启动动画。有一长列内嵌的游戏动画列表,您可以在参考部分中找到完整列表的链接。清单 12 包含一个小的脚本实例,当您触摸时,您就会鼓掌。

清单 12. 欢呼(Applause)按钮
default {
  touch(integer total) {
    key avatar = llDetectedKey(0);
    llRequestPermissions(avatar, PERMISSION_TRIGGER_ANIMATION);    
  }
  run_time_permissions(integer perm) {
    if (perm & PERMISSION_TRIGGER_ANIMATION)  {
    	llStartAnimation("clap");
    }
  }
}

“鼓掌”动画将持续运行,您可以调用 llStopAnimation("clap") 来中断。要查看您是否仍旧在运行此动画,调用 llGetAnimationList

字符串“鼓掌(clap)”标识所有者知道的动画,在这种情况下是预加载到游戏中的默认动画。您可以上载新的自定义动画,这样您就可以完全控制游戏化身的动作。上载新的内容要求您提供少量的 Linden Dollars,本教程中不介绍这些。

迷你游戏

介绍完 LSL 基础知识之后,是时候创建 Second Life 中的迷你游戏了。

游戏如何进行

该游戏应该这样玩:您将图腾掉在地上。通过触摸激活图腾,从它顶部出来 6 个有色的球:3 个红色的,3 个蓝色的。图腾确定了一个半径 10 米的圈,表示游戏范围,然后游戏开始了。每个玩家(红蓝两方)尽可能快地进行游戏,每次他们触碰自己的球时,游戏就向前推进。如果他们撞了对手的球,并且将其撞出游戏圈,而自己没出去,那么对手的球就成自己的,并且将该球重新设置在游戏圈内的随机位置上。如果玩家将自己的球撞出圈,那么复位。如果玩家将对手的球撞到了圈外,同时与另一个对手的球碰撞了,那么该球无人领取,而只是重新设置回圈内。如果玩家的球撞到了对手,但自己出了圈,该球归对手。这些是一个非常简单的游戏的规则,但它们设法将 Second Life 和 Linden Script 中一些更有趣的部分合并起来。

在您开始写代码之前,用一点点您对 Linden Script 所了解的东西,设计您需要的物体,以及它们如何交互。

设计物体

您需要两类物体:游戏图腾,和游戏球。游戏图腾负责管理游戏状态:开始和停止游戏、创建并清理球、确定胜利并与玩家沟通。游戏球的工作是追踪它自己的颜色、能够交换颜色、撞其他球、响应正在游戏的玩家化身的触碰和碰撞,并且响应图腾发出的命令,复位到圈内的命令,以及清除自己的命令。

最后的设计目标,接收来自游戏图腾的命令,是您最终必须用的模式的结果。原因是一个物体不能任意地移动另一个物体,它只能友善地发送消息,请求该物体自己移动。其他方法类似,包括 llDie,它摧毁调用的物体。所以,对于您的游戏的位置重设部分,以及物体清除,游戏球物体必须响应来自图腾的消息,但自己完成所有工作。

因为您有 2 个物体类型,所以您需要 2 个脚本,每个脚本分别为每种类型的物体定义状态和事件处理程序:游戏图腾,和游戏球。对于我的实现,我决定游戏球不需要多个状态,如果您愿意可以将颜色交换实现为状态变更。然而游戏图腾必须至少有 2 个状态,以便您可以很容易地管理游戏。它拥有默认状态,相当于‘stopped’,并且它拥有‘started’状态,类似于上面实例中的状态。

当您创建完游戏球脚本和游戏图腾脚本之后,将游戏球赋予游戏图腾,这样游戏图腾就有权生成游戏球的副本了,从此,您应该做的所有事情就是生成游戏图腾,并激活,然后它将处理余下的工作。

游戏球

在 Rational 中打开一个新文件,将其命名为 gameball.lsl(参见清单 11)。

清单 11. gameball.lsl
// last_collide will serve as a marker to help us enforce game rules
string last_collide = "";

// this keeps track of our listening key, 0 means not listenting, > 0 
// is a unique identifier
integer listen_id = 0;

// reset is called to clean up our variables
// and make sure we are properly listening
reset_listener () {  
  // if we had a listener already, remove it first
  if (listen_id > 0) {
    llListenRemove (listen_id);
    listen_id = 0;
  }
  // now register a listener for channel 1235  
  listen_id = llListen (1235, "", "", ""); 
 
}

// reset position is used by the listen event handler
// it moves us back into the game, using llSetPos ()
reset_position (float x, float y, float z) {
  // turn off physics while we teleport the object
  llSetStatus (1, FALSE);
  // if physics is on, you can't use llSetPos ()
  // since you must create and apply forces to account for any movement
  // but with it off, we can simply set the position without fuss.
  llSetPos (<x, y, z+2>); // start 3 meters above the game totem
  // turn physics back on and drop the ball on the totem
  llSetStatus (1, TRUE); 
}

// reset the ball's current color, this is used by listen, in reaction to a reset message
// implement critical game logic as well, since it determines if colors should swap based 
// on who hit who last
reset_color () {
  // if the last collision was 
  // a game ball of some kind but not our own color
  if ((last_collide == "redgameball" || last_collide == "bluegameball")
    && llGetObjectName () != last_collide ) {
    // swap our color
    if (llGetObjectName () == "redgameball") {
      llSetColor (<0, 0, 255>, ALL_SIDES);
      llSetObjectName ("bluegameball");
    } else if (llGetObjectName () == "bluegameball") {
      llSetColor (<255, 0, 0>, ALL_SIDES);
      llSetObjectName ("redgameball");
    }
  }
}

default { 
  // when the gameball is created enforce some standards on the ball
  state_entry () {
    // the object we are attached to must be a regulation size
    llSetPrimitiveParams ([PRIM_SIZE, <.6,.6,.6>]);
    // and must be a 90% hollow sphere
    llSetPrimitiveParams ([PRIM_TYPE, PRIM_TYPE_SPHERE, 
      0, // holeshape
      <0.0, 1.0, 0.0>, // cut
      90.0, // hollow
      <0.0, 0.0, 0.0>, // twist
      <0.0, 1.0, 0.0> // dimple
    ]);

    if (llGetObjectName () == "Object") {           
      llSetObjectName ("gameball");
    }
  
    // it must obey physical laws
    llSetStatus (1, TRUE);
  
    // it must use a blank texture 
    // from lswiki: this is the key of the 'Blank' texture
    llSetTexture ("5748decc-f629-461c-9a36-a35a221fe21f", ALL_SIDES);
    // attach our listener
    reset_listener ();
    // forget about previous collisions
    last_collide = "";

  }

  // when the ball is created
  on_rez (integer start_param) {
    // use the optional start_param to determine which color
    reset_listener ();
    // forget about previous collisions
    last_collide = "";

    if (start_param == 0) {
      llSetColor (<0, 0, 255>, ALL_SIDES);
      llSetObjectName ("bluegameball");
    } else {
      llSetColor (<255, 0, 0>, ALL_SIDES);
      llSetObjectName ("redgameball");
    }
  }
  // when the ball collides with something
  collision_start (integer total_number) {
    string other = llDetectedName (0);
    // if we collided with some other gameball, write it down
    if (other == "redgameball" || other == "bluegameball") {
      last_collide = other;
    } else if (llDetectedType (0) & AGENT) {
      // if we collided with an agent, consider ourselves kicked.
      last_collide = (string) llDetectedKey (0);
      // this code gets a vector of magnitude 2, 
      // pointing away from the avatar that kicked us.
      vector kick_vector = llGetPos () - llDetectedPos (0);
      kick_vector.z = 0;
      kick_vector = llVecNorm (kick_vector)*2;
      // use this vector as a force vector and apply it to the ball
      llApplyImpulse (kick_vector, FALSE);
    }
  }
  // when the ball is touched by someone
  touch (integer total_number) {
    last_collide = "";
    vector kick_vector = llGetPos () - llDetectedPos (0);
    // scale to a fixed magnitude: 2 in the case of a touch
    kick_vector.z = 0;
    kick_vector = llVecNorm (kick_vector)*2;
    kick_vector.z = .1;
    llApplyImpulse (kick_vector, FALSE);
  }
  listen (integer channel, string name, key id, string message) {
    // llParseString2List will split the input message on spaces
    list cmdline = llParseString2List (message, [" "], [" "]);
    // the first param must be this object's key
    string k = llList2String (cmdline, 0);
    if (k == (string) llGetKey ()) {
      string s = llList2String (cmdline, 1);
      llWhisper (0, "Gameball received command: "+s);                
      if (s == "reset") {
        // if it is, then we need to reset
        float x = llList2Float (cmdline, 2);
        float y = llList2Float (cmdline, 3);
        float z = llList2Float (cmdline, 4);
        reset_color ();
        reset_position (x, y, z);
        last_collide = ""; // reset the last collision
      }
      else if (s == "die") {
        llDie ();
      }
    }
  }
}

现在,有许多要看的代码,但这些超过了游戏的一半,因此您不要害怕,您差不多都看到了。(在这种规模上,您就可以开始看到在 Rational Application Developer 中构建脚本的优势了,如您在图 10 中所见,彩色编码和大纲视图令代码更容易管理。)

图 10. Rational Application Developer 中的脚本
Rational Application Developer 中的脚本
Rational Application Developer 中的脚本

让我们从更高层观察代码,每次深入一步。

从基本的观点看,您定义了只有一个状态(default)和五个事件处理程序的物体:state_entryon_rezlistentouch,和 collision_start

由于首先调用 state_entry 且只调用一次,所以用它来设置物体的静态属性,执行公平游戏,让球为规定尺寸,并且用一致的结构造型。

当该物体的新实例出现在世界中时,调用 on_rez 事件,一般是由于 llRezObject(由 gametotem 调用的,将在后面看到),但也可能是由于从您的储备中拖拽‘游戏球’到屏幕上,或者由于将游戏球附着到您身体的某个点上。在这种情况下,您希望用该事件来确保球有颜色,并且登记了它的监听者,以便在玩球时接收 listen 事件。

listen 事件极其重要,并且值得更详细地研究。

当调用 listen 时,是由于文本的消息到达了您预订的信道(使用 reset_listener 调用)。在这种情况下,在私用信道 1235 上接收来自 gametotem 物体的特殊格式化的消息。在对这些 listen 事件的响应中,您使用一些非常有用的 LSL 方法来处理格式化数据的分析:llParseString2ListllList2String,和 llList2Float

在您私有的小协议中,在信道上发送的消息总是以标识消息目标的键作为前缀。由游戏图腾发出的典型‘die’消息可能是:“5748decc-f629-461c-9a36-a35a221fe21f die”。该消息可能发送给监听信道 1235 的范围之内的所有人。每个监听者只负责监听发送给它自己的消息。如果您熟悉 Ethernet 连网,那么相似的比喻是 MAC 地址在 Ethernet 帧中隐藏的方式,以及包广播到局域网进行传递的方式。在将包传递给软件更高层进行处理之前,每个 Ethernet 设备都要查看 MAC 地址是否与自己的匹配。

在 listen 事件中的每个游戏球都必须标记并保证第一个标记是自己的键。这正式 llParseString2List 做的事情,根据作为参数传递的分隔符标记字符串,llList2String 返回字符串形式的带索引的列表项,并允许您从第一个位置开始检索并核实键。再次使用 llList2String 检索第二个参数,命令名称。如果该消息是 reset 命令,那么类似地使用 llList2Float 从指定了应该将您的位置重新设置在哪里的消息中分析浮点数,另外,如果是“die”消息,使用 llDie 来摧毁该物体。

对于您在前面看到的简单实例来说 touchcollision_start 事件几乎是一样的。它们虑及了两种不同的方式,将游戏部件推来推去。碰撞事件实现了游戏逻辑的关键部分,并且它记住了您最后碰撞的东西。如果您在 listen 事件中检索“reset”消息,那么就会用到此值来确定您是否应该切换颜色。

刚才说了那么多游戏球的事了。让我们来看看游戏图腾脚本,然后将它们放在一起,玩游戏。

游戏图腾

在 Rational 中,打开新文件,gametotem.lsl(参见清单 12)。

清单 12. gametotem.lsl
// game_field_size defines the range of the game perimeter
integer game_field_size = 10;
// the list of balls we have spawned, so we know who to clean up
list balls = [];
// a function to spawn one ball
spawn_ball(integer is_red) {
  // turn physics off, so the totem isn't shifted around
  llSetStatus(1, FALSE);
  // spawn a new object, "gameball" must be in this object's inventory 
  llRezObject("gameball", llGetPos()+<0,0,1>, 
    <-3+llFrand(6),-3+llFrand(6),1>, ZERO_ROTATION, is_red);
  llSetStatus(1, TRUE);
}
// a function to sends a message using the private channel
send_message(key child_id, string message) {
  llShout(1235, (string)child_id+" "+message);
}

// function to cleanup all the balls we have spawned
despawn_balls() {
  integer i;
  for(i = 0; i < llGetListLength(balls); i++) {
    send_message(llList2Key(balls, i), "die");
  }
}
// function to reset our scanner and gameballs
reset() {
  // stop a previous sensor if there was one
  llSensorRemove();

  // cleanup any previous balls that might be around
  despawn_balls();
  
  // spawn 3 red balls, and 3 blue balls
  spawn_ball(TRUE);
  spawn_ball(TRUE);
  spawn_ball(TRUE);
  spawn_ball(FALSE);
  spawn_ball(FALSE);
  spawn_ball(FALSE);
  
  // set a green message over the totem's head
  llSetText("Let the game begin!", <0,255,0>, 1.0);
  
  // turn on our sensor that actually runs the game logic every 1.0 sec
  llSensorRepeat("","",ACTIVE,200.0, 3.1415926, 1.0);
}

default {
  state_entry() {
    llSetText("Touch me to start.", <255, 255,255>, 1.0);
  }
  
  on_rez(integer param) {
    llSetText("Touch me to start.", <255, 255,255>, 1.0);
  }
  // touching the totem turns the game on and off.  
  touch(integer total) {
    llSetText("Starting...", <255, 255,255>, 1.0);
    state started;
  }
}

state started {
  state_entry() {
    reset();
  }


  // object_rez is fired to notify us that 
  // llRezObject successfully spawned a new child
  object_rez(key child_id) {
    // keep track of the new child, for later clean up
    balls += child_id;
  }
  // touching the totem turns the game on and off.
  touch(integer total) {
    // clean up any spawned balls
    despawn_balls();
    llSetText("Touch me to start.", <255,255,255>, 1.0);
    // switch to the default 'stopped' state
    state default;
  }
  sensor(integer total) {
    integer blue_count = 0;
    integer red_count = 0;
    integer i;
    for(i = 0; i < total; i ++) {
      string name = llDetectedName(i);
      if( name == "redgameball" || name == "bluegameball" ) {
        vector pos = llGetPos();
        float dist = llVecDist(pos, llDetectedPos(i));
        if( dist > game_field_size ) {
          // send a message out on the game channel: "reset"
          // all the nearby listen events will fire
          // but the key filter in gameball's listen handler
          // makes sure the only the correct ball resets
          send_message((string)llDetectedKey(i),"reset "
            +(string)(pos.x)+" "+(string)pos.y+" "+(string)pos.z);
        }
      }
    }
    // check the victory condition
    for(i = 0; i < total; i ++) {
      if( llDetectedName(i) == "redgameball" ) {
        red_count++;
      } else if( llDetectedName(i) == "bluegameball" ) {
        blue_count++;
      }
    }
    if( red_count == 0 ) {
      llSetText("Blue wins! Touch to reset.", <255,0,0>, 1.0);
      llSensorRemove();
    } else if( blue_count == 0 ) {
      llSetText("Red wins! Touch to reset.", <255,0,0>, 1.0);
      llSensorRemove();
    }
  }
}

比起游戏球来说,游戏图腾相当简单。图腾脚本有两个状态,default 和 started,当您触碰图腾时,在两个状态间切换。

除了一些记帐事件以外,实际的游戏逻辑在每秒都循环的 sensor 事件中发生。您可以看到,传感器在顶部的 reset 方法中首先调用 llSensorRepeat。每次循环的迭代都会向那些图腾范围之外的游戏球发出 reset 消息。这是游戏球中的 listen 事件实际开始的地方。

现在所有的部件都就位了,这两个脚本应该是您玩新的迷你游戏所需的所有内容。

将它们放在一起

既然您已经看到了所涉及的脚本,那么从 Rational 中拿出脚本,并插在一些实际的游戏物体中。

  1. 将脚本导入到您的储备中,这样您就可以很容易地将它们拖拽。单击底栏的 Inventory 按钮,右键单击 Scripts 文件夹,并选择 New Script
  2. 游戏中的脚本编辑器打开了。清空,并粘贴 gameball.lsl 的内容。
  3. 将其保存为 gameball。它应该出现在您的储备的 Script 文件夹之下。
  4. 对 gametotem.lsl 重复上面操作,将其保存为 gametotem。现在 Scripts 下应该列出了 gametotem 和 gameball。
  5. 右键单击地面并从饼形菜单中选择 Create
  6. 选择任意物体形状,不论您选择什么,它都将转换为规定的游戏球。
  7. 当您使用创建笔,单击左键并生成新的 Prim 之后,到 Editor 的 Content 选项卡中,将 gameball 脚本从您的 Inventory 拖到 Contents 窗格中。一旦脚本连接到物体上,它应该调用 state_entry 方法,并迫使该物体变为规定大小的空球。
  8. 查看物体正确地命名为 gameball。单击右键,并在饼形菜单中选择 Take。游戏球应该出现在 Inventory 中的 Objects 下(参见图 11)。
    图 11. 游戏球
    游戏球
    游戏球

在物体编辑器中创建图腾。

  1. 选择 Create wand。
  2. 选择您想要的任何形状(不存在对图腾是什么物体的约束条件)在世界中单击左键,并将其放在地面上。
  3. 到新物体的 Content 选项卡中,将游戏球物体从 Inventory 中拖到 content 窗格中。对游戏图腾脚本做同样的操作(参见图 12)。
    图 12. 完整的游戏图腾的 Content 选项卡
    完整的游戏图腾的 Content 选项卡
    完整的游戏图腾的 Content 选项卡
  4. 在连接了脚本之后,新的图腾物体上应该显示出白色的文本和消息“Touch me to start”(参见图 13)。
    图 13. 游戏图腾
    游戏图腾
    游戏图腾

触摸图腾,应该生成一批游戏球,并且让您撞击球来进行游戏。在您删除图腾之前,再次触摸图腾,清除生成的所有球(参见图 14)。

图 14. 启动游戏
启动游戏
启动游戏

总结

Second Life 环境便于它自己构建任何东西。在本教程中,您首先了解了如何使用 Rational Application Server 令开发变得更容易,然后您就转移到基本的脚本编写上了。您了解了如何创建基本的动画,以及进行碰撞检测,这是大多数游戏的命脉。您还了解了如何让玩家化身活起来。然后您将这些合在一起,创建了可以作为单个图腾执行的多部分的游戏。

有了这些技术,您可以构建任何虚拟的东西。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Rational, Open source
ArticleID=258670
ArticleTitle=用 Rational Application Developer 创建 Second Life(第二人生)脚本
publish-date=09272007