• 全部 专栏 Game Jam专区
    • 红茶君 2231 6

      让玩家前秒掀桌,后秒叫好的游戏创作术

      让玩家前秒掀桌,后秒叫好的游戏创作术 by 蔡建毅

      据说日本有个老家伙,自称是全世界最懒的游戏设计师。不爱玩游戏,连自己做的游戏也没认真玩完。日常生活呢,也没什么爱好,连自己老婆都看不下去。你说这么懒散的家伙,能做出什么名堂呢?

      且慢,请看下列他的部份作品列表:

      重装机兵,制作人
      天外魔境2 监督,剧本
      越过我的尸体1,2 游戏设计,剧本
      啪嗒砰,世界观,剧本监修
      魔王勇者,记录的地平线 总监修


      他的作品一开始,就毫不客气地把游戏常识放地上踩:勇者一出场不是马上挂掉就是快要挂掉,要不然砸颗陨石过来,告诉你世界就要毁灭,别玩什么魔王勇者家家酒,赶快建方舟搜集动物拯救世界吧!再不然让国王下诏令-谁把到最多妹,谁就是下一任国王!


      他的游戏卖相通常不怎样,没有精美画面,更别提尖端技术,但总是靠口碑,颠覆性剧情,以及绝佳游戏性创下游戏长卖佳绩。相信看到这,识货的人肯定扬起孔明般的微笑吧!

      是的,这位曾经创下无数长卖作品的幕后人物,正是传说中的鬼才-桝田省治。

      而本系列文章,正是介绍这位表面上漫不经心的老前辈,台面下如何用各种缜密的人性欲望设计,与游戏系统紧密结合的叙事手法,设下重重陷阱来紧紧抓住玩家的心。如何做出这种前一秒让玩家掀桌大骂,后一秒拍桌叫好的神级游戏呢?就让我们继续看下去…


    • Bowie 2238

      IL2CPP 深入讲解:P/Invoke封装

      IL2CPP 深入讲解:P/Invoke封装IL2CPP INTERNALS: P/INVOKE WRAPPERS

      (译注:P/Invoke,全称是platform invoke service,平台调用服务,简单的说就是允许托管代码调用在 DLL 中实现的非托管函数。而在这期间一个重要的工作就是marshall:让托管代码中的数据和原生代码中的数据可以相互访问。我在下文中都称之为内存转换。)


      这是IL2CPP深入讲解的第六篇。在这篇文章里,我们会讨论il2cpp.exe是如何生成在托管代码和原生代码间进行交互操作而使用到的封装函数和类型。特别的,我们将深入探讨blittable和non-blittable之间的区别,理解String,Array数据在内存上的转换,以及了解这些转换所付出的代价。

      我编写托管和原生间的交互代码已经有好一段时间了,但是要让p/invoke在C#中的声明始终保持正确是一件很困难的事情。理解运行时那些对象是如何在内存上进行处理的就更加令人感觉神秘了。因为IL2CPP在这方面为我们做了绝大部分的工作,我们可以查看(甚至调试)这些内存转换行为,为我们处理问题和效率分析提供良好的支持。


      这篇文章不会提供内存转换或者是原生代码交互的基础介绍。这是一个非常宽泛的话题,一篇博文根本不可能放得下。Unity的官方文档有讨论原生插件是如何与Unity交互的。Mono和Microsoft也对p/invoke提供了足够多的信息。

      老生常谈了:在这个系列中,我们所探索的代码都很有可能在以后的Unity版本中发生变化。然而不管代码怎么变,其基础的概念是不会改变的。所以这个系列中的所有讨论的代码都属于实现细节。

      项目设置

      在这篇文章中,我使用的是Unity 5.0.2p4在OSX上的版本,目标平台是iOS,编译构架上我选择的是“通用”(“Universal”)。最终我会使用XCode 6.3.2来为ARMv7和ARM64编译代码。

      首先我们看看原生代码:

      #include
      #include

      extern "C" {
      int Increment(int i) {
      return i + 1;
      }

      bool StringsMatch(const char* l, const char* r) {
      return strcmp(l, r) == 0;
      }

      struct Vector {
      float x;
      float y;
      float z;
      };

      float ComputeLength(Vector v) {
      return sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
      }

      void SetX(Vector* v, float value) {
      v->x = value;
      }

      struct Boss {
      char* name;
      int health;
      };

      bool IsBossDead(Boss b) {
      return b.health == 0;
      }

      int SumArrayElements(int* elements, int size) {
      int sum = 0;
      for (int i = 0; i sum += elements;
      }
      return sum;
      }

      int SumBossHealth(Boss* bosses, int size) {
      int sum = 0;
      for (int i = 0; i sum += bosses.health;
      }
      return sum;
      }

      }

      在Unity中的托管代码仍然在HelloWorld.cs文件中:
      void Start () {
      Debug.Log (string.Format ("Using a blittable argument: {0}", Increment (42)));
      Debug.Log (string.Format ("Marshaling strings: {0}", StringsMatch ("Hello", "Goodbye")));

      var vector = new Vector (1.0f, 2.0f, 3.0f);
      Debug.Log (string.Format ("Marshaling a blittable struct: {0}", ComputeLength (vector)));
      SetX (ref vector, 42.0f);
      Debug.Log (string.Format ("Marshaling a blittable struct by reference: {0}", vector.x));

      Debug.Log (string.Format ("Marshaling a non-blittable struct: {0}", IsBossDead (new Boss("Final Boss", 100))));

      int[] values = {1, 2, 3, 4};
      Debug.Log(string.Format("Marshaling an array: {0}", SumArrayElements(values, values.Length)));
      Boss[] bosses = {new Boss("First Boss", 25), new Boss("Second Boss", 45)};
      Debug.Log(string.Format("Marshaling an array by reference: {0}", SumBossHealth(bosses, bosses.Length)));
      }

      cs代码中的每一个函数最终都会调用到上面原生代码的一个函数中。后面我们将逐一分析每一个托管函数的申明。

      为啥需要内存转换?
      既然IL2CPP已经把C#代码都变成了C++代码,我们干嘛还需要从C#做内存转换到C++?虽然生成的C++代码是原生代码,但是在某些情况下,C#中数据类型的呈现还是和C++有所区别的,因此IL2CPP在运行的时候必须在两边来回转换。il2cpp.exe对数据类型和方法都会做相同的转换操作。


      在托管代码层面,所有的数据类型都被分为两类:blittable或者non-blittable。blittable类型意味着在托管和原生代码中,内存的表现是一致的,没有区别(比如:byte,int,float)。Non-blittable类型在两者中的内存表现就不一致。(比如:bool,string,array)。正因为这样,blittable类型数据能够直接传递给原生代码,但是non-blittable类型就需要做转换工作了。而这个转换工作很自然的就牵扯到新内存的分配。


      为了告诉托管编译器某些函数是在原生代码中实现的,我们需要使用“extern”关键字。使用这个关键字,和“DllImport”属性相配合,使得托管的运行时库能够找到原生中的函数并且调用他们。il2cpp.exe会为每一个extern函数产生一个封装。这层封装执行了以下一些很重要的任务:




      为原生代码生成一个typedef以用来通过函数指针进行函数调用。

      通过名字找到原生代码中的函数,并且将其赋值给一个函数指针

      如果有必要,将托管代码中的参数内存转换到原生代码格式

      调用原生函数

      如果有必要,将原生函数的返回值内存转换到托管代码的格式

      如果有必要,还需要处理具有关键字是“out”或者“ref”的参数,将他们的内容从原生格式转换到托管代码格式。


      下面我们就来看看产生的这些封装函数都是什么个情况。


      内存转换blittable数据类型

      最简单的extern封装只牵扯到blittable类型。

      [DllImport("__Internal")]
      private extern static int Increment(int value);
      在Bulk_Assembly-CSharp_0.cpp文件中,查找“HelloWorld_Increment_m3”函数。为“Increment”提供封装的函数像下面这个样子:

      extern "C" {int32_t DEFAULT_CALL Increment(int32_t);}
      extern "C" int32_t HelloWorld_Increment_m3 (Object_t * __this /* static, unused */, int32_t ___value, const MethodInfo* method)
      {
      typedef int32_t (DEFAULT_CALL *PInvokeFunc) (int32_t);
      static PInvokeFunc _il2cpp_pinvoke_func;
      if (!_il2cpp_pinvoke_func)
      {
      _il2cpp_pinvoke_func = (PInvokeFunc)Increment;
      if (_il2cpp_pinvoke_func == NULL)
      {
      il2cpp_codegen_raise_exception(il2cpp_codegen_get_not_supported_exception("Unable to find method for p/invoke: 'Increment'"));
      }
      }

      int32_t _return_value = _il2cpp_pinvoke_func(___value);

      return _return_value;
      }

      首先,我们来一个typedef:
      typedef int32_t (DEFAULT_CALL *PInvokeFunc) (int32_t);

      其他的封装函数一开始看起来也差不多会是这样。在这里,这个*PInvokeFunc 是一个有int32参数并且返回一个int32的函数指针。

      接下来,封装尝试找到对应的函数并且将其地址赋值给这个函数指针

      _il2cpp_pinvoke_func = (PInvokeFunc)Increment;

      而实际的Increment函数是通过extern关键字表明它处在C++代码中。

      extern "C" {int32_t DEFAULT_CALL Increment(int32_t);}

      在iOS平台上,原生函数会被静态的链接到单一的bin文件中(通过在DllImport中的“__Internal”关键字),因此IL2CPP运行时并不需要动态的查找相应的函数指针。相反,这部分工作是在link期间完成的。在其他平台上,IL2CPP可能会根据需要进行函数指针的查找。

      事实情况是:在iOS平台,非正确的p/invoke在c++编译器link的阶段就会体现出来而不是等到运行时才发现。因此所有的p/invoke都必须正确,哪怕他们实际没有被执行到。

      最终,原生代码通过函数指针被调用,函数的返回值被送回托管代码中。请注意在上面的例子中,参数是按值传递的,所以任何对参数值的改变都不会最终印象到托管代码中。


      内存转换non-blittable类型

      当处理non-blittable数据类型比如string的时候,事情会变得更加有趣。还记得前面文中提到的吗?在IL2CPP中string实际上是一个通过UTF-16编码的,最前面加上了一个4字节前缀的,两字节宽的数组。这种内存格式和C中的char*或者wchar_t*都不兼容,因此我们必须做一些转换。如果我们看一下StringsMatch函数(在生成代码中叫HelloWorld_StringsMatch_m4):

      DllImport("__Internal")]
      [return: MarshalAs(UnmanagedType.U1)]
      private extern static bool StringsMatch([MarshalAs(UnmanagedType.LPStr)]string l, [MarshalAs(UnmanagedType.LPStr)]string r);

      我们会发现每一个string参数都会被转换成char*(通过UnmangedType.LPStr指令)。

      typedef uint8_t (DEFAULT_CALL *PInvokeFunc) (char*, char*);

      具体的转换看上去是这样的(对于第一个参数而言):

      char* ____l_marshaled = { 0 };
      ____l_marshaled = il2cpp_codegen_marshal_string(___l);

      一个适当长度的char内存块被分配,将string中的内容拷贝到新的内存中。当然,当函数执行完毕后,我们会将这个内存块释放。


      il2cpp_codegen_marshal_free(____l_marshaled);
      ____l_marshaled = NULL;

      因此内存转换像string这样的non-blittable类型是一个费时的操作。

      内存转换用户自定义类型

      像int或者是string这样的类型还算好理解,那么如果有更加复杂的用户自定义类型会发生什么呢?假设我们想对有着三个float的Vector类型进行内存转换,我们会发现如果一个自定义结构中的所有成员都是blittable的话,这个类型就可以作为blittable来对待。因此我们可以直接调用ComputeLength(在生成的代码中叫HelloWorld_ComputeLength_m5)而不用对参数做任何转换。

      typedef float (DEFAULT_CALL *PInvokeFunc) (Vector_t1 );

      // I’ve omitted the function pointer code.

      float _return_value = _il2cpp_pinvoke_func(___v);
      return _return_value;

      同样的,参数是按值传递的,就像上面那个int的例子一样。如果我们想改变Vector的值,我们必须按引用传递这个变量,就像下面SetX函数(HelloWorld_SetX_m6)所做的那样:

      typedef float (DEFAULT_CALL *PInvokeFunc) (Vector_t1 *, float);

      Vector_t1 * ____v_marshaled = { 0 };
      Vector_t1 ____v_marshaled_dereferenced = { 0 };
      ____v_marshaled_dereferenced = *___v;
      ____v_marshaled = &____v_marshaled_dereferenced;

      float _return_value = _il2cpp_pinvoke_func(____v_marshaled, ___value);

      Vector_t1 ____v_result_dereferenced = { 0 };
      Vector_t1 * ____v_result = &____v_result_dereferenced;
      *____v_result = *____v_marshaled;
      *___v = *____v_result;

      return _return_value;

      作为引用的话,参数在原生代码中就变成了指针,所生成的代码也有一些繁琐。本质上,代码会创建相同类型的局部变量,将参数中的内容拷贝到此局部变量,然后用此局部变量指针作为参数调用原生函数,在函数返回后,将局部变量的值拷贝回参数变量中以便让托管代码访问到变化后的值。

      内存转换non-blittable用户自定义类型

      列子中的Boss这样的non-blittable用户自定义类型也是可以做内存转换的。但是需要更多一些的工作:类型中的每一个成员都必须单独的转换成原生的表现形式。再进一步,生成的C++代码中必须要有和原生代码中表现一致的自定义结构。

      让我们来看一下IsBossDead声明:



      [DllImport("__Internal")]
      [return: MarshalAs(UnmanagedType.U1)]
      private extern static bool IsBossDead(Boss b);

      这个函数的封装是HelloWorld_IsBossDead_m7:

      extern "C" bool HelloWorld_IsBossDead_m7 (Object_t * __this /* static, unused */, Boss_t2 ___b, const MethodInfo* method)
      {
      typedef uint8_t (DEFAULT_CALL *PInvokeFunc) (Boss_t2_marshaled);

      Boss_t2_marshaled ____b_marshaled = { 0 };
      Boss_t2_marshal(___b, ____b_marshaled);
      uint8_t _return_value = _il2cpp_pinvoke_func(____b_marshaled);
      Boss_t2_marshal_cleanup(____b_marshaled);

      return _return_value;
      }

      传递封装函数的参数是Boss_t2,和托管代码中的Boss结构相对应。但是在传递给原生函数的时候Boss_t2_marshaled。如果我们跳转到这个类型的定义,我们会发现Boss_t2_marshaled和原生C++库中的Boss类型的定义是一致的:

      struct Boss_t2_marshaled
      {
      char* ___name_0;
      int32_t ___health_1;
      };

      我们还是使用UnmanagedType.LPStr在C#中来指引string转换成char*。如果你发现在调试non-blittable用户自定义类型时有困难。在生成的代码中查看一下带_marshaled后缀的结构会很有帮助。如果结构和原生代码中的结构不一致,那么内存转换肯定会出问题。

      上面的例子中,Boss_t2_marshal函数用来对Boss类中的每个成员进行转换。而Boss_t2_marshal_cleanup则负责进行清除工作。


      内存转换数组

      最后,我们来看一下如果内存转换blittable和non-blittable的数组。SumArrayElements传递的是一个整数型数组:


      [DllImport("__Internal")]
      private extern static int SumArrayElements(int[] elements, int size);

      数组会进行内存转换,不过因为其每个元素都是blittable的int形,转换的代价是非常小的:


      int32_t* ____elements_marshaled = { 0 };
      ____elements_marshaled = il2cpp_codegen_marshal_array((Il2CppCodeGenArray*)___elements);

      il2cpp_codegen_marshal_array函数仅仅是返回托管代码中数组的首地址。

      然而,内存转换non-blittable类型的数组开销就会大得多。SumBossHealth函数传递的是一个Boss数组:


      [DllImport("__Internal")]
      private extern static int SumBossHealth(Boss[] bosses, int size);

      封装不得不分配一个新数组,然后对数组中的每一个元素都做一次内存转换:

      Boss_t2_marshaled* ____bosses_marshaled = { 0 };
      size_t ____bosses_Length = 0;
      if (___bosses != NULL)
      {
      ____bosses_Length = ((Il2CppCodeGenArray*)___bosses)->max_length;
      ____bosses_marshaled = il2cpp_codegen_marshal_allocate_array(____bosses_Length);
      }

      for (int i = 0; i {
      Boss_t2 const& item = *reinterpret_cast(SZArrayLdElema((Il2CppCodeGenArray*)___bosses, i));
      Boss_t2_marshal(item, (____bosses_marshaled));
      }

      当然,我们还必须在函数调用完成后进行内存释放操作。


      结论

      在内存转换上, IL2CPP的行为和Mono是一致的。因为IL2CPP会对extern函数和类型产生封装代码,因此我们可以检查交互调用的开销。对于blittable而言,开销通常还好,但是对于non-blittable而言,会让开销上升的很快。我们只是对内存转换做了个简单的介绍。有关返回值和带out关键字的参数,原生函数指针和托管中的代理,用户自定义的引用类型的内存转换,还请大家探索源码自行分析。

      下一篇文章我们将探索IL2CPP和垃圾回收器的集成。


      原文链接

    • 红茶君 2161

      「去月球」这样的故事是如何创作出来的?

      导语:应该有不少人玩过「去月球」(To The Moon)这个游戏。这是个很有意思的游戏,你会发现在盛赞这个游戏的群体中,既有平时不怎么玩游戏的女孩子,也有对各种主机大作和经典游戏非常熟悉的核心玩家。按照开发者高瞰的观点,这是因为「故事是最能广泛地被受众接受的一种表达」。而我认为除此之外,开发者清晰的目标和出色的执行,才是使这个看似简陋的游戏完整而有感染力的关键,对于独立开发者来说,「去月球」在这方面是一个很好的标杆。我们在本期中整理并翻译了「去月球」的开发者高瞰在2012年西雅图Casual Connect上的一次分享,分享主题是:Keeping It Personal -- Bringing Human Stories to Games.从这个分享中可以稍稍看见一个优秀的故事创作者的创作思路。以下是分享正文。
      大家好,我叫高瞰。
      我大约在五、六年前开始做游戏,而在两年前我决定以我一直钟爱的游戏事业为生,然后我做出了我的第一个商业优秀「去月球」。这个作品和我创作的其他游戏一样,都是比较个人化的作品。
      今天我要讲的主题是说故事。为了讲得更有条理一些,我会按顺序介绍以下几个主题:「故事及功能」,「人与讲故事」、「工具和传达」、「宗旨和目的」。
      首先来说故事。什么是故事呢?可能没人会这么问。故事的基础就是一系列事件用叙事来展现,但它的核心是交流和沟通。讲故事是一种很特别的沟通方式,甚至可以说是独一无二的,别的沟通方式传递数据或信息,而故事可以传递思想和情感状态。你可以分享人生中一个非同寻常的时刻所感受到的愉悦,也可以分享你的期盼与失落,或者你也可以做一个像我一样的混蛋,毫无理由地让别人感到沮丧……无论如何,这真的很有成就感。
      故事对你的游戏最大的益处是:一个关于人的故事是最能广泛地被受众接受的。正如你所见,如今的游戏玩家群体非常复杂,通常被分为:休闲玩家,中核玩家,和硬核玩家。详细的分类太多太杂,我甚至都数不过来。但是绝大数玩家都是人类(还有部分不是人,见下图)。 所以如果你的叙事能让人产生共鸣,那么它一定能被受众接受。另一些形式的艺术,比如美术和音乐,也具有同样的力量。但从某种意义上讲,这两种艺术形式也是一种叙事:当人们聆听音乐或欣赏画作时,受众会根据自身经历去想象一个故事。而当故事与美术和音乐结合、再加上互动的元素,它就会具有无比的感染力——这就是游戏。

      因此,说故事不仅仅是在传达剧情,也是建立你与玩家的一种连接。事实上,你与玩家之间经常会有「断连」现象存在:你想表达的独特情感,与玩家根据自身经历所理解的情感并不相同。说故事最关键的是要保持与受众间的「双向沟通」,这很难,因为你在写故事时无法听取受众的感受。
      那么,什么是「人的故事」呢?首先所有故事都能被称为人的故事,因为任何故事的基础都关乎如何与人产生联系。对于正在看故事的人来说,人的故事本质上是一种「不再孤独」的感觉:他们想体会他们所经历的脆弱、焦虑、愉悦和渴望等一系列感情,而这些情感也是其他人所体验过的——这是人的本能,而且故事恰恰可以很好地传达它。
      以下是常常能在人的故事中找到的一些特点:个人化(personal)、令人共鸣(relatable),令人信服(authentic),整全的情感光谱(complete spectrum)。
      先说说个人化这一特点。最关键的是说故事必须要有侧重点,专注于传达故事的主题,以及你希望观众理解的情感。为了让这件事变得更容易实现,故事里的角色就不会太多,这样每个角色都能充分地被塑造。即使在那些大型史诗级游戏里——比如「龙腾世纪」——都会把侧重点放到少部分角色和他们的塑造上。
      关于令人共鸣的特点。一般故事很难通过用相似的经历让玩家共鸣,通常是通过表达一些令人共鸣的主题。所以,只要故事的主题与玩家的经历有一些共通性,就会令人产生共鸣。拿「去月球」来举例,玩家不太可能在现实中经历一模一样的情节,但使这个游戏与玩家共鸣的是以下这些主题:人与人的连接、沟通、误解……这些都是每个人日常生活中的一部分。这也是为什么通过表达主题,游戏能让人联想到自身的经历,而这种联想带来的情感传达,是比作者讲述的故事本身要强烈的。
      接下来讲情感光谱。「飞出个未来」(Futurama)就是一部在这方面做得很棒的动画片。这个动画的优秀之处在于它从不会在感人的场面过于拖沓。它通常侧重于喜剧的表达,但是,当感人场景出现,就会在观众心中产生强烈的反差效果,这加强了它给人们带来的情感连接和感情价值,也给观众造成意料之外的惊喜。如果当你体验一部作品前,有人告诉你它会让你感动落泪,你通常会在不经意间树立起感情上的防御壁垒,避免自己被感动,这或许是为了让自己显得比较特殊……或许不是……但当你提起戒心时,你在作品面前就不再是一个自然状态了。
      然后,故事要令人信服。这并不是说要写自传式的故事,但是作品的灵感来源要来自于内心真实的感受、或是作者能够产生很深连接的事物。因为只有这样,你才能真正理解并沉浸于这些感情,甚至将其重新塑造并用其他方式表达出来。作者完全可以展示自己内心个人化的、脆弱的一面。
      有一个方法可以判断你写的故事是否已经足够影响人心:看看你周围有没有人因为这个故事和你认真较劲,他们可能会被你的故事打动,但最有的还是让你滚蛋……不过这都没关系。
      接下来讲「工具和执行」。其中包括「视角代入和自圆其说」,「统一各方面」和「移情与预判玩家反应」。
      首先,每个人看问题的角度都是不同的,因为在考虑视角问题的时候,不可能把所有人的角度都考虑一遍。然而,你可以给玩家一个机会,去承认属于他们的角度,告诉他们:我知道这个故事没有照你们的预想进行,也明白你们暂时无法理解它为何如此发展,我理解你们的感受。即便是坚定地拒绝也好:不,我知道你虽然这么想,但是故事不会这样发展。即使这一个否认也可以消除玩家被忽略的感受。对一个游戏来说,忽略玩家的感受就是最大的失误。当然,这种安排必须和游戏相和谐,不能太刻意。
      在「去月球」里,两个博士的主要作用就是负责承认玩家的感受。现在看来,我的处理还不够完美,因为它有点过于明显。不过,至少这种处理表达了我的意愿。这个安排可以看做是一个剧中剧,当玩家在探索Johnny的一生时,两个医生从最初就与玩家一直相伴,他们就是玩家感受的投射,他们的存在使玩家不至于觉得自己的感受被忽略、或者游戏中还有什么未被提及。
      游戏中元素的结合统一也很关键。这一点对大型游戏项目来说比较困难,但对于独立游戏来说却是一个优势。因为当游戏各部分分开来做就会出现这样的情况:有专门负责故事的团队,有专门负责美术的团队…每个人各自为政,最后把成品组合起来。其实游戏中许多元素之间的配合是无法提前预计的。
      比如,在「去月球」中,有个典型的例子就是游戏配乐:我本人不是一个专业的作曲家,如果你从音乐的角度去分析游戏中的曲子,会发现其实它们并无特殊之处。但是,当你把音乐和具体的故事结合起来——由于它们相互间是高度配合的,所以会得到非常出色的效果。正如你们所见,游戏原声中有30首曲子,其中有三分之一的曲子有着同样的主题旋律,这个主题来自于同一首曲子。伴随着游戏进行,这个主题会在潜意识中影响玩家,造成整体上的统一感。这样,这个游戏独特的个性,也随游戏的展开而逐渐展现在玩家面前。
      当然还有一些难以预见的优点。在制作以叙事为核心的独立游戏时,最困难的部分之一就是游戏中的人物资源,因为这些资源不能像其他形式的资源那样很方便地复用。所以我把「去月球」中回忆世界里的非核心人物全都模糊化了,采用这种方法,我可能节约了将近一百多个人物的制作成本。
      我们的制作团队在游戏的每个方面都紧密地合作,并且在开发的早期就建立起了游戏各部分之间的紧密连接,而不是在最后一刻才将它们摆在一起。类似的方法有很多,都可以帮助你节省开发成本,提高制作效率。
      然后讲「移情和期待」。讲故事是一个双向的过程,由于开发者无法直接聆听玩家的感受,所以在每个故事的关键点与玩家保持同步就变得非常重要。你需要了解玩家会处于怎样的心理状态,然后对其进行回应。
      有一个我还未曾尝试过的方法,不过应该会比较有效:用一张图表把故事中的关键点标出来,然后在这些地方写出玩家所处的心理状态,以便作者去设计相应的回应。而对心理状态最好的回应方法是:要么反映它,要么颠覆它。
      如果选择去反映它,你可以随着剧情的进展而表达情感,并不断与玩家建立关系;如果选择颠覆它,就能添加喜剧色彩。但是介于两者之间的手法就不会给人留下太多印象,也不会很有用。
      最后说设计故事的目的。其实在如今的大环境中,剧情往往只是游戏的副属品,游戏制作者常常觉得,游戏剧情就是让游戏更有趣一些,或者增加一些东西让玩家能更感兴趣。但这并不是设计故事的目的,因为无法定义什么是「有趣」,也无法找到要实现的目标。
      然而,如果你着眼于沟通,着眼于让人体会一个真实的情感状态,让玩家在玩游戏时自然地体会到这种状态,最终结果是游戏也自然变得有趣起来。
      在演讲的最后,我想说:在每个人的生命中,都有太多可以成为故事的东西,这不仅包括那些疯狂的、很厉害的经历,也包括我们每天的日常。只要用心去体会和发现,你会找到一个合适的角度去表达它。谢谢。

    • totoyan 1285 1

      【活动】傅老师的Construct2引擎游戏制作工作坊(广州站)[广州]

      您想学做游戏吗?小时候就想做游戏,至今还没完成?有意学做2D游戏,却不知道如何开始?已有美术基础,但看不懂密密麻麻的代码方程? 就让我们用 3小时 教会你做手机游戏吧! 在网页技术发达的今天,HTML5游戏已成为手机游戏的流行风向标。不但简单易学,一次开发便可横跨多平台。艾空未来这次请到台湾资深游戏导师傅老师一同为大家分享,3小时内就要让你的游戏在手机与计算机上运行。让我们一起掌握最新技术,完成梦想中的游戏吧!
      * 本次课程包含上机实作,请来宾务必自备计算机(Windows系统)。

      活动时间:
      2015年8月27日 18:00~22:00 活动地点:广州市天河区员村西街广纺联创意园纺联东一路自编45号之182房
      活动内容:1、Indie Game奥义心法分享行业、环境、生活的痛点都是我们创意的入口,不断寻找并体验着身边的一切,为创意插上飞翔的翅膀创造梦想的价值。我们的游戏,H5游戏的切入,IndieGame经验分享主讲人:iconplus创始人&创意总监:刘惠斌 2、H5极速开发工具Construct 2体验分享带领来宾以Construct 2游戏引擎进行开发,让您轻松大步跨入H5游戏开发的世界。本次课程以经典平台游戏为主轴,了解原理后现场就将范例改造为无限跑酷游戏。主讲人:恒进学习中心 傅子恒老师 3、来宾发问与Debug时间来宾可将使用Construct 2开发上碰到的问题进行现场提问,我们将进行具体且详细的答疑(由于时间有限,每位来宾的问答时间约在3~5min) 4、签名时间与会人员如果带上傅老师的《Construct游戏程序设计》,也可来让老师签下大名留念哦!嘿嘿
      2015年8月27日(星期四)活动时间流程
      17:30~18:05    参会签到
      18:05~18:10    主持人开场、致辞
      18:10~19:30    iconplus创意总监刘惠斌经验分享
      19:30~21:00    恒进学习中心傅子恒老师运用C2一次完成游戏分享
      21:00~21:30    来宾DEBUG或提问,现场答疑

      参与活动的人员: 1. 如果你是努力进修的网页前/后端攻城狮2. 如果你是想成为Indie Game的热血青年3. 如果你是HTML5开发者4. 如果你是学校教师、讲师5. 如果你混迹于各大游戏论坛6. 如果你是C2论坛吧贴的粉丝7. 如果你也是傅老师的铁杆粉丝 参与活动需求:参加活动的小伙伴能带上自己的 笔记本或电脑 尤佳,可以一边听分享一边跟着傅老师的节奏嗨起来,一个属于你的游戏就会如奇迹般的诞生了! 由于本次活动是 iconplus 赞助,小伙伴们自发组织的,时间与活动安排都比较紧迫,很多不足的地方还需大家体谅,对于人数的安排,我们暂定在广州 icon.cn,如果与会人数超出将在安排场点再另发出通知,如有不便敬请见谅!另外来自台湾的傅老师工作比较繁忙,很感激老师百忙之中抽空来给我们大家做介绍与分享!^-^ 请快点来加入我们的大家庭吧,参与本次的活动,一起寻找游戏的真谛与快乐!活动联系人:
      totoyan 手机号:15920188041 QQ:2851683686电子邮件:yzt@icon.cn活动QQ群 :180911504



    • 红茶君 1497

      资深GameJam玩家告诉你,如何把GameJam游戏送上主机平台

      导语:本文根据开发者高鸣在上个月的IndieACE游戏开发者沙龙上的分享整理。他的游戏作品「蜡烛人」诞生于一次Gamejam,如今在补充和完善之后将要登上Xbox One主机。高鸣作为一个资深的GameJam玩家,对如何玩好一次GameJam这件事颇有心得。而这样的GameJam经验对于游戏开发者日常的开发,都有借鉴的价值。
      以下是他的分享。
      大家好。我今天的分享主题是「怎么把Gamejam游戏送上主机平台?」。我要讲的分为三个部分:一是为什么要参加GameJam,第二是一个GameJam游戏是如何登上主机平台的,最后分享一些玩GameJam的经验。
      首先,为何参加Gamejam?Gamejam是一个极限游戏开发的行为,通常是在48小时内单人或组队做完一个游戏;名字里有个jam,所以聚会也是它的重要属性,GameJam是一场游戏开发者的聚会。
      基于这两个核心属性,Gamejam能给我们带来什么呢?第一,因为它是游戏极限开发行为,所以可以锻炼游戏开发技术。游戏制作是一门艺术,也是一门手艺,手艺要做好就要多做。现实中,成功的团队可能做过好几款不成功的游戏,很多大公司也会组织员工开发很多游戏原型,这都是在积累游戏制作的经验。
      Gamejam给了游戏开发者一个机会,可以全方面地锻炼你做游戏的技艺。因为时间很短,要做的事又很多,所以无论你想锻炼哪方面的开发技能,都能得到锻炼机会;GameJam的另一个好处,是拓展你的边界。做游戏开发,深度很重要,广度也很重要。如果你是游戏设计师,你应该多了解各个方面的知识,不仅仅是游戏设计相关的知识,就像程序员需要不断学习新的语言和技术一样。GameJam对游戏开发者来说,就是一个拓展边界的好机会,在活动中,策划做美术、程序做策划的情况经常会出现;你也很可能因为一个特别的主题,去尝试之前没有试过的游戏风格和类型。这种活动促使你拓宽自己的边界,找到新的可能。
      我自己就在一次Gamejam上做了一个自己永远不会尝试的游戏类型,类似瓦里奥制造那样的小游戏集合,游戏画风是基于现实的照片。作为设计者,这是非常有意思的经验。
      GameJam的第三个收获是,能够获得可复用的经验,这种经验不仅可以在将来的Gamejam中使用,更可以用在将来的游戏开发中。因为Gamejam相当于浓缩版的独立游戏开发行为。
      为什么这么说呢?独立开发有几个特点:1,资源有限;2,重视创意;3,大道至简,比大作更多地做减法。反观gamejam,在任何一个方面,都非常符合这三点,所以算是独立开发的威力加强版:GameJam中时间和人员都有限;为了在众多作品中脱颖而出,你的游戏要重视创意;而在时间和资源有限的情况下,不做减法是做不完的。当你不断地参加Gamejam,你实际上是一次又一次地参加日常的独立游戏开发活动。
      「蜡烛人」的诞生就得益于参加GameJam活动的经验。「蜡烛人」那次活动的主题是「10秒」:「蜡烛人」的设计思路就是只有10秒钟的光,可以点亮自己照亮周围,点完了蜡烛就烧完了,你可以继续往前走,但是之后就是在黑暗中前进;当时除了蜡烛人之外,其实还有另一个游戏创意,按照那个创意开发的另一个游戏都已经开发得差不多了,网站上也很多人点赞,但我还是是果断地选择了蜡烛人,因为无论是在游戏的创新度上,还是在美术资源的需求量上,蜡烛人都是更有优势、更容易掌握的。
      「蜡烛人」的开发过程中还运用了很多既往的经验,比如做减法。我在进行开发前画了个故事板,一共画了6个画面,按照这个数量是做不完的,所以我砍掉了和叙事相关的开头和结尾,只保留了关卡选择和关卡游戏这两个部分。也正因如此,我能够有更多的时间开发更多的关卡来表现游戏玩法。
      另一个就是,我们最终的蜡烛人是两只腿,但是一开始是三条腿,因为在现实中实在找不到两只腿的蜡烛。最后因为三条腿不好做,所以做成了两条。
      「蜡烛人」的所有制作,包括三维贴图、关卡等等全是在48小时内完成的,如果没有之前的经验的话,这是不可能做到的。
      GameJam能够给我们什么收获呢?首先,无论如何,48小时后你会做完一款游戏。开发者们都知道「做完一款游戏」这件事有多么难得。
      因为GameJam是带有交流的属性的开发者聚会,志同道合的游戏开发者可以齐聚一堂,大聊要做什么游戏然后把它实现,这也是非常可贵的。游戏开发是比较孤独苦闷的事,所以就如同人类发明节日一样,同类聚在一起,可以排解孤独。对于开发者来说,大家聚一聚意义很大。
      参加一次成功的Gamejam还有一个额外的收获,就是能给你的游戏带来反馈。比如每次参加Ludum Dare,都能在网上收到好多条反馈。Gamejam的现场试玩也能得到很多开发者的反馈。如今,你把一个游戏上传到App store上,都无法保证能够及时地得到玩家反馈,但参加Gamejam可以立即得到其他开发者得回应。凭这一点,GameJam就是值得所有游戏开发者向往的活动。
      「蜡烛人」之所以有机会登上Xbox,也得益于Gamejam活动的评分和反馈机制。参加Ludum Dare之后,你能够得到你的游戏在所有游戏中的排名;因为Ludum Dare的参加者众多,所以能进入前100都是十分可贵的事了;而蜡烛人的排名是:总评27;创意26;画面30;可玩性40;氛围97;一共六项成绩进入前100;这个成绩之前我从来没有得过,看到之后我就有一种感觉:这个游戏和之前不一样,是不是可以做点什么?
      之后我决定给这个游戏一个机会,继续开发开发,然后动用了我所在工作室的人力,完善了这款游戏,把画面、操作、关卡、摄像机等等细节都补充完善了。这个游戏的主要特色就是:低光的游戏氛围、非常有限的亮光、主角很弱,你要努力过关。游戏氛围很不安,因为光很弱,所以有一个投机取巧的地方在于,美术资源虽然不多,但并不感觉简陋,玩起来也比较有意思。完善之后我把它提交到了国外一个在线游戏平台叫kongregate,在这个版本发布之后,获得了这个网站的首页推荐,在首页呆了两周,得到了很多曝光率和很多玩家的好评。「蜡烛人」再一次在kongregate这个平台上证明了自己的实力。
      后来也没有刻意继续开发这个游戏,因为没有想好一个合适的故事,所以没有给它继续分配更多的资源和人力。之后Xbox进入中国,我们团队在申请ID@Xbox的时候投了两款游戏,一个是花了很长时间做的一个手机跑酷过关的游戏,另一个就是「蜡烛人」,后来「蜡烛人」胜出了。现在,「蜡烛人」的Xbox One版本在稳步开发中,目前已经解决故事的问题,并且给它想了很多围绕着光与影的新玩法。今年年底或许能看到这款游戏。
      为什么「蜡烛人」能从一个Gamejam游戏变成一个主机游戏?首先,Gamejam催生了这个游戏:「10秒钟」这个主题给了它一个创意,并且让这个原型在48小时内完成;之后,Gamejam非常有效的反馈机制让我在结束后发现这个游戏的闪光点和可能性。
      在此之后并不是一个励志故事,我们没有特别对这个游戏不离不弃,毕竟我是Gamejam的常客,在类似活动中也完成过十几个游戏。可以说「蜡烛人」不是坚持的结果,而是自然选择的结果。把所有做完的Gamejam游戏扔在那,「蜡烛人」通过自然选择证明了自己的价值,使我不得不重视它,不得不把它做好。后面的发展都是顺水推舟,因为一个好的游戏原型创意想法自己会发光,会给自己机会。蜡烛人就是通过Gamejam产生的大量游戏原型,然后自然选择的结果。
      最后分享一些Gamejam的经验。首先在赛前,推荐给大家一个比较奇特的绝招:通过微博或者微信等等社交媒体告诉你的朋友你要参加Gamejam,你要在48小时之后拿出一个游戏。在参赛过程中你可能会经常觉得你的进度特别落后,第二天你会觉得你做不完,你想放弃,但是你已经把牛吹出去了,所以你必须做完。这个赛前的准备会在最后阶段逼你把游戏做完。这个招数屡试不爽,而且通过这种方式,也可以顺便推广Gamejam这样的活动;第二,在设计阶段,开始写代码画图之前,遵循keep it simple的原则,你可以天马行空地想很多想法,但是在总结的时候要列两个清单:一个清单是这款游戏必不可少的内容、那些去掉之后游戏就不成立的内容;另一个是锦上添花的内容。最后制作的时候还发现砍得不够多,那么这里有一个电梯游说原则:假如你在电梯里遇见一个投资人,能不能用一句话对他说清楚这个游戏是什么。留下这个最本质的东西,就是你的游戏。
      建议在开发前再上一层保险,就是绘制游戏流程的故事板。把你游戏可能出现的画面画出来,然后直观地通过画面的数量判断游戏的复杂程度。如果有10个画面,一定是做不完的,最好控制在5个以下,2、3个是最好的。
      在原型开发阶段,先做个最小可行化产品(MVP),核心玩法+核心画面=最低可操作可发布版本,这就是一个最小的可以玩的游戏,这个游戏之后再做更多内容都是可以的。即使后面的内容来不及,你也有一个可玩的游戏了。
      谢谢大家。
      高鸣所在的游戏团队叫做“交典创艺”,除了「蜡烛人」这样有趣的主机平台的游戏之外,他们还有两款品质和卖相俱佳的游戏在寻找代理发行资源。点击这里可以查看详细情况。感谢大家。

    • 红茶君 1711

      「快斩狂刀」开发者单隽:一个游戏人的十年

      导语:前段时间看到有媒体人写文章回忆10年前的ChinaJoy,十年间人来人往,喧闹依然。对于我们当中的一些人来说,确实亲身经历过国内游戏业的十年甚至更长的时间,见证过不同的人群有幸成名或是黯然离开,其中感受或许难以简单说清。
      在上个月的IndieACE开发者沙龙上,「风卷残云」和「快斩狂刀」的开发者单隽就稍稍分享了他在近十年中经历过的一些事。他分享的主题是「独立游戏的商业化之路」,而这个故事令人欣慰的地方在于,正是由于商业的发展,更多的人在商业环境中获得了「做自己想做的游戏」的自由。
      以下是根据单隽的分享整理的文字稿。
      2011年有个叫「风卷残云」的游戏在各平台发布,反响还不错,直到现在每个月还能提供上千块的收入,虽然这个收入显然不能支持一个20人的团队,但这件事给我们提供了制作游戏的动力。
      很多独立开发者可能都会面临一个问题:毫无疑问做独立游戏很爽,但我要不要不工作全心投入开发,我要不要拿投资让自己全身心投入?
      我分享一些自身的经历。我大学的时候就在开发独立游戏,1999年的时候,并没有独立游戏这个概念,只有游戏开发爱好者。我高中就开始开发游戏,当时是参照市面上的一些流行游戏来做的,目的是和同班同学交流。而大学的时候有更强的自我意识,希望开发出一款代表自己风格的作品。那时开发了大概4、5款游戏,拿到校级比赛大奖,其中一款就是「风卷残云」前身,当时的设想是游戏中有武器、潜入、近身搏斗等等元素,后来强化了搏斗这部分,最后就成了风卷残云。当时花了一个半月做了原型,几个同学在一起玩非常快乐,我一直觉得我想追求的就是这种感觉。
      大学最撼动我的经历是,当时做了一个以机器人为题材的游戏,游戏灵感来自于,我发现有时自己不玩游戏、光看朋友打游戏也很爽,所以想开发一款玩的人很开心、看的人也很开心的游戏。然后就花十个月的时间开发了一款机器人游戏,每人养成一个五人战队,战前配置好机器人、预先设置好机器人的行动,战斗中不能操作。
      这个游戏在校级比赛里拿了冠军,后来学校社团拿这个游戏在学校里办了个比赛,选出16个参赛选手参加比赛,活动现场全场爆满。比赛过程中,很多玩家没有按我的设计思路去正常地玩,所以在实际战斗中出现了很多非常滑稽欢乐的场面,大家看得都很开心。两个小时的比赛没有一个人中途离开,作为一个社团活动来说是一件很难得的事。
      这件事让我感触很深。我发现自己独立开发一个游戏,能影响到他人,这种感觉非常打动我。当时觉得比起考试读书,做游戏给我带来的满足感要大得多。这种感受一直影响我到如今。
      大约在2001年、2002年的时候,我要毕业了,那时刚好是中国游戏商业化开始迈向成熟的时期,有「传奇」这样的作品,也有盛大这样的网络游戏公司。当时希望从商业游戏的角度看看自己的作品,试试能不能做一款商业上成功的游戏,于是加入了上海盛大网络。
      我还记得盛大校招宣讲的时候,一个盛大的领导说他们马上要代理破碎银河系,他说,什么是破碎银河系呢?就是星际争霸里每一个小兵都是一个玩家来扮演。这件事让我觉得当时做商业游戏的人,也具备审美眼光,所以凭借之前做游戏的优势,开始了这份工作。
      当时在盛大做传奇世界,工作非常努力,希望有一天能在盛大这样的平台上开发出一款有自己风格的作品。经过了一年多的努力,有机会做主策划和项目经理,就做了一个叫三国豪侠传的游戏,在这个项目上投入了非常多的心血,但是这个游戏在商业上惨败。原因在于我们花了很多时间做兵种配合等等要素,想让战棋游戏变得更好玩,但是我们忽视了中国玩家对成长和积累需求,当玩家没法凭借等级优势等等在游戏中的积累压制别人时,就没有继续玩下去的动力。项目失败之后我就离开了盛大,这个结果也是理所当然的,当时项目中有很多比我年长、希望通过这个项目拿到奖金和期权的人,而我把它们的希望毁了。
      之后我投奔了一个朋友的公司,上海灵禅,灵禅虽然做很多外包,这段经历也和做独立游戏关系不大,但当时灵禅有很多圈内的前辈,在这里工作我学到了很多管理经验,学会了怎么协调各人利益。
      到了2006年,市场上出现了Live Arcade的模式,出现了像PopCap这样的公司,我们发现游戏可以用一个让人上瘾的核心模式+付费解锁这种形式来赚钱,这意味着有一种不用再做规模化程式化的成长和数值系统,就能养活自己的游戏模式。当时Xbox360想进中国,而我们想在海外发作品证明自己的实力,所以就合作了。我回顾自己开发游戏的方式,就想能不能尝试回到大学的时候,用独立游戏开发的方式来做这个游戏:自己亲手写demo,一个星期之内确定玩法,一个月之内把主要风险的东西解决掉。后来做到了,后来这个作品成为中国人制作的第一个上XBLA的游戏,但是项目做到一半的时候我离开了团队。这是2007年。
      然后开始做「风卷残云」。「风卷残云」这个游戏是2011年发布,但其实2007年就开始了开发,前后大约四年的时间,前三年都是拿自己的积蓄开发的。开发这个游戏的经历让我对自己的追求有了更深的认识。
      最初一段时间,团队一共只有两个人,最早的计划是用9个月做一款XBLA的游戏,无论销量如何都能支撑后续开发,但实际上做这个游戏花了三年多。
      两个人做游戏的那段时间是非常美好的回忆,在那种状态里能和很多公司进行非常坦诚的交流,可以不隐藏任何东西,因为我们做的游戏和所有人都不一样,所以大家都是朋友而不是竞争对手,感觉非常幸福。但是两个人做游戏也给商业化带来了很多门槛,我们的游戏每次比赛都获奖,但没有什么实际作用,因为运营商发行商之类的合作伙伴会不放心,担心两个人能否支撑一个游戏的持续运营。相信直到如今对于一些小的独立团队来说,这种情况也存在。
      游戏做到第三年的时候,已经弹尽粮绝了,我们和一些投资方聊了很久。我们的游戏也越来越成熟完善,看到了商业化的可能,甚至有人觉得这个品质可以用来做大型MMO游戏。对于我们那么小的团队来说,这种肯定是非常有安全、非常受鼓舞的事情。再后来带着游戏在ChinaJoy上展出,有媒体对我们进行了报道,在我们开放游戏demo下载的当天,由于下载人数太多,撑爆了服务器,在3DM上出现了一大堆关于「风卷残云」的讨论,然后出版商和运营商就都来找我们了。后来我们以一个十人的团队做完并发行了这款游戏,游戏还登上了XBLA和Steam。
      之后也经历过很多挣扎,可以说在这个过程中,商业化是不得不考虑的问题。现在我们有一个20人的团队,我们叫上海擎月软件科技有限公司,正在做的游戏叫「快斩狂刀」。找我交流可以通过邮箱联系我:19591273@qq.com。谢谢。 这是「快斩狂刀」正在开发中的PS4版本,CJ现场的玩家试玩得很开心。

    • 红茶君 1519

      雷亚的游戏制作方法论

      这篇文章是根据7月份的IndieACE沙龙上,来自台湾雷亚的开发者游名扬的分享整理的。
      小团队或者个人开发者或许经常遇到这样的状况:想出了一个很妙的点子,大张旗鼓地开始制作,却发现无法量产…或者,反复修改原型,甚至做出了完成度颇高的demo,却总觉得哪里不好玩…雷亚作为创造了Cytus、Deemo、Implosion等优秀作品的团队,其实开发中遇到的问题和大家没什么不同——这些就像每个开发者必交的学费,在经历一番摸爬滚打,创造了许多的坑之后,才能磨合出自己的感觉和节奏。
      雷亚的游总想用亲身经历告诉大家的是:看准你作品的核心价值所在,然后抓住它不要放手。
      现在IndieACE把这篇分享整理出来,希望能对各位朋友有些启发。
      我从09年开始进入游戏行业,一开始是帮游乐场做街机和大型游戏机的游戏。
      我在08年的时候做了第一款自己的独立游戏,程序美术都是自己写的,那款游戏可以说是Cytus的前身。不过大家要明白一个道理,当你的作品放到市场上,没人管你是不是独立游戏,如果画面不好看音乐不好听,就卖不出去。
      我在台大念的是计算机系,擅长做软件,硬件知识比较弱,所以11年决定出来自己开公司,重点就在做软件,开发软件的话,App Store是最好的平台,因为发行门槛低,可以把游戏发行向全世界。我们做的Cytus、deemo和Implosion,在应用商店里,在超过100个国家和地区被推荐过。
      Implosion开发了三年半,我们的合伙人从未婚到结婚到小孩出生,游戏才做出来,所以真的做了很长的时间。
      我们发布了四款产品后,苹果给了我们一个开发者专区,说明苹果在支持原创方面,还是发挥了很大作用的。
      我们玩游戏的时候经常会碰到卡关的情况。其实身为游戏开发者,在开发过程中也常常遇到卡关,甚至我们会发现,开发中卡关和解决的时间,占到了开发时间的一半以上。
      卡关了怎么办呢?我们通常把游戏开发的过程,归纳为三个阶段:prototype、pre-production和production,也就是原型制作-量产前置-量产。游戏是互动和艺术的结合,如果说电影的表达靠的是美术、镜头和剧情来给观众代入感,而体育享受的是一种纯互动的乐趣,那游戏的坐标就在他们当中,是两者的结合。所以在原型制作最重要的是确定核心玩法,在这个阶段找到游戏的核心价值;找到之后,你会发现这个核心价值不一定能量产,比如你的作品的核心价值在于非常华丽但复杂的美术,可能就是难以量产的。
      在量产前置阶段,开发者要寻找把1变成100的量产方法,想办法把制作变为可能;这个过程通常不是直线进行的,是不断互相循环的,你可能在量产前置阶段发现一些问题,最后要不断回到原型制作阶段去修改和解决。
      举个例子,Cytus是我们做的一款音乐游戏,游戏玩法描述起来很简单,就是进入主选单-选歌曲-玩歌曲-结算;最重要的是核心玩法的部分,Cytus的核心价值是全触碰的音乐游戏和它不错的美术画面;事实上,在这个核心玩法之外,游戏制作过程中的每一个改动也都涉及思考和验证阶段。
      而Deemo的核心玩法的idea就来自于,要和Cytus做出区别。我受到一款叫做Magic Piano(魔法钢琴)的App的启发,觉得音乐游戏中有自己弹钢琴的感觉会更好。虽然Cytus用到了全屏幕,但我们观察一下当下最红的音乐游戏,主要还是用落下式的玩法,所以我们确定了钢琴+落下式的法+黑白概念的游戏,因为想用极简的风格做出钢琴的感觉。
      按照这个思路做了原型,甚至进入了量产前置阶段,然后让玩家玩了一下,发现玩了一两首歌觉得很好,多玩几首就乏味了,没办法让玩家持续玩下去,总感觉少了点东西;另一个问题是游戏里的钢琴是没有力度的,力度难以表达,没有代入感。
      关于力度的部分,我们找了很多参考和音乐模拟游戏,看他们怎么做,用一个暴力的方法解决了,就是每个音都取样,取4个力度,用4组声音去还原这四个力度。
      另外一方面,除了钢琴之外加点别的,加什么呢?决定加故事,有了故事就有无限发展的可能;加角色,加了一个角色叫孤独的deemo,他原本是黑白的,演奏之后变彩色;还有一颗树,演奏后会生长。一个角色不够,就再加一个妹子给他。
      有了两个人的互动,故事就有发展了:女孩掉进这个世界里,她如何回家。正因为有了故事这个要素,每个曲子就能用两个人的互动呈现。现在网上有很多同人,也在想象两人能发生怎样的互动,画师也在画里埋梗;进行到这里,游戏在极简之外,就多了一些人文的成分。
      但音乐游戏要怎么讲故事呢?我们的做法是加入探索功能,游戏到了不同的阶段,游戏场景会变化。在场景发生变化前,强迫玩家看一段动画,这样故事就变成有意义的东西,动画播完后回到房间,场景发生变化,让玩家去探索,这样玩家就看到了故事。
      再举个例子,Cytus是我们的第一款产品,核心玩法就是全屏幕的触碰,它的判定比以往的音乐游戏都大,但正是因为太简单了,音乐游戏的大触们无法得到成就感。然而,如果我们更新一个版本把判定调严格的话,原本的轻度玩家会有挫折感。所以最后我们加了一个判定,计算TP(Technical Point),在显示上不是特别明显,在意它的人会看到,不在意的人不会发现。现在这个游戏的强者都在比这个。
      另一个例子是Implosion(聚爆),这也是我们开发过程中挫折感最大的一个故事:我们做的第一版游戏界面,元素非常多,各种副属性主属性堆在一起,还有晶片作用描述等等;选单又藏得太深,描述性文字特别多。用户体验不好,最后我们把这一版界面全换掉了。玩家最需要的体验其实是:很快看到想要的东西,方便地更换装备,所以最后设计成了左右相连滑动的画面,没有上一层下一层的跳转设计。 一开始我们的开发流程是:RD用简单的方块做了一个雏形,UI设计师再去画,完成之后再review成果是否符合预期,不行就推翻重来。这种流程会在循环中浪费很多时间,不符合敏捷开发的原则,所以当时一个选单花了四个月才做完。这是什么问题呢?是review的机制有问题,最好的是有一个负责用户体验的人,随时检查,随时给下决定的人看,这样更有效率。
      还有一个更挫折的事,一开始聚爆是给Xbox360做的游戏,但是交涉没那么顺利,考虑到在主机上也不一定能很成功,最后决定把主机版砍掉,做成了一个手机游戏。这也是导致这个游戏做了很久的原因之一。
      所以总结我们的经验就是:原型制作阶段不要怕砍掉重练,但确定原型之后在作业期要保持贪心,尽量满足所有要做的事情,尽量保持随时review的机制。最重要的是记住游戏的核心价值,核心价值确定之后,所有的工作都是为了实现它,不要因为眼前的卡关牺牲核心价值。

    • Bowie 1579

      IL2CPP 深入讲解:泛型共享

      IL2CPP 深入讲解:泛型共享IL2CPP INTERNALS: GENERIC SHARING IMPLEMENTATION 这是 IL2CPP深入讲解的第五篇。在上一篇中,我们有说到由IL2CPP产生的C++代码是如何进行各种不同的方法调用的。而在本篇中,我们则会讲解这些C++方法是如何被实现的。特别的,我们会对一个非常重要的特性 -- 泛型共享 加以诠释。泛型共享使得泛型函数可以共享一段通用的代码实现。这对于减少由IL2CPP产生的可执行文件的尺寸有非常大的帮助。


      需要指出的是泛型共享不是一个新鲜事物,Mono和.Net运行时库(译注:这里说的.Net运行时库指的是微软官方的实现)也同样采用泛型共享技术。IL2CPP起初并不支持泛型共享,我们到最近的改进版中才使得泛型共享机制足够的健壮并能使其带来好处。既然il2cpp.exe产生C++代码,我们可以分析这些代码来了解泛型共享机制是如何实现的。
      我们将探索对于引用类型或者值类型而言,泛型函数在何种情况下会进行泛型共享,而在何种情况下不会。我们也会讨论泛型参数是如何影响到泛型共享机制的。
      请记住,所有以下的讨论都是细节上的实现。这里的讨论和所涉及的代码很有可能在未来发生改变。只要有可能,我们都会对这些细节进行探讨。
      什么是泛型共享
      思考一下如果你在C#中写一个List的实现。这个List的实现会根据T的类型不同而不同么?对于List的Add函数而言,List和List会是一样的代码么?那如果是List呢?
      实际上,泛型的强大之处在于这些C#的实现都是共享的,List泛型类可以适用于任何的T类型。但是当C#代码转换成可执行代码,比如Mono的汇编代码或者由IL2CPP产生的C++代码的时候会发生什么呢?我们能在这两个层面上也实现Add函数的代码共享么?
      答案是肯定的,我们能在大多数的情况下做到共享。正如本文后面将要讨论的:泛型函数的泛型共享与否主要取决于这个T的大小如何。如果T是任何的引用类型(像string或者是object),那T的尺寸永远是一个指针的大小。如果T是一个值类型(比如int或者DateTime),大小会不一样,情况也会相对复杂。代码能共享的越多,那么最终可执行文件的尺寸就越小。
      在Mono中实现了泛型共享的大师:Mark Probst,有一个关于Mono如何进行泛型共享的很棒的系列文章。我们在这里不会对Mono的实现深入到那么的底层中去。相反,我们讨论IL2CPP是怎么做的。希望这些信息可以帮助到你们去更好的理解和分析你们项目最终的尺寸。
      IL2CPP的共享是啥样子的?
      就目前而言, 当SomeGenericType中的T是下面的情况时,IL2CPP会对泛型函数进行泛型共享: 任何引用类型(例如:string,object,或者用户自定义的类) 任何整数或者是枚举类型
      当T是其他值类型的时候,IL2PP是不会进行泛型共享的。因为这个时候类型的大小会很不一样。
      实际的情况是,对于新加入使用的SomeGenericType,如果T是引用类型,那么它对于最终的可执行代码的尺寸几乎是没有影响的。然而,如果新加入的T是直类型,那就会影响到尺寸。这个逻辑对于Mono和IL2CPP都适用。如果你想知道的更多,请继续往下读,到了说实现细节的时候了!
      项目搭建
      这里我会在Windows上使用Unity 5.0.2p1版本,并且将平台设置到WebGL上。在构建设置中将“Development Player”选项打开,并且将“Enable Exceptions”选项设置成“None”。在这篇文章的例子代码中,有一个驱动函数在一开始就把我们要分析的泛型类型的实例创建好。








      public void DemonstrateGenericSharing() { var usesAString = new GenericType(); var usesAClass = new GenericType(); var usesAValueType = new GenericType(); var interfaceConstrainedType = new InterfaceConstrainedGenericType();}

      接下来我们定义在这个函数中用到的泛型类:






























      class GenericType { public T UsesGenericParameter(T value) { return value; } public void DoesNotUseGenericParameter() {} public U UsesDifferentGenericParameter(U value) { return value; }} class AnyClass {} interface AnswerFinderInterface { int ComputeAnswer();} class ExperimentWithInterface : AnswerFinderInterface { public int ComputeAnswer() { return 42; }} class InterfaceConstrainedGenericType where T : AnswerFinderInterface { public int FindTheAnswer(T experiment) { return experiment.ComputeAnswer(); }}以上这些代码都放在一个叫做HelloWorld的类中,此类继承于MonoBehaviour。
      如果你查看il2cpp.exe的命令行,你会发现命令行中是不带本系列第一篇博文所说的--enable-generic-sharing参数的。虽然没有这个参数,但是泛型共享还是会发生,那是因为我们将其变成了默认打开的选项。
      引用类型的泛型共享
      让我们从最常发生的泛型共享情况开始吧:对于引用类型的泛型共享。由于所有的引用类型都是从System.Object继承过来的。因此对于C++代码而言,这些类型都是从Object_t类型继承而来。所有的引用类型在C++中都能以Object_t*作为替代。一会儿我们会讲到什么这点非常重要。
      让我们搜索一下DemonstrateGenericSharing函数的泛型版本。在我的项目中,它被命名为HelloWorld_DemonstrateGenericSharing_m4。通过CTags工具,我们可以跳到GenericType的构造函数:GenericType_1__ctor_m8。请注意,这个函数实际上是一个#define定义,这个#define又把我们引向另一个函数:GenericType_1__ctor_m10447_gshared。
      让我们往回跳两次(译注:使用CTags工具,代码关系往回回溯两次)。可以找到GenericType 类型的申明。如果我们对其构造函数GenericType_1__ctor_m9进行追溯,我们同样能够看到一个#define定义,而这个定义最终引向了同一个函数:GenericType_1__ctor_m10447_gshared。
      如果我们跳到GenericType_1__ctor_m10447_gshared的定义,我们能从代码上面的注释得出一个信息:这个C++函数对应的是C#中的HelloWorld::GenericType`1::.ctor()。这是GenericType类型的标准构造函数。这种类型称之为全共享类型,意味着对于GenericType而言,只要T是引用类型,所有的函数都使用同一份代码。
      在这个构造函数往下一点,你应该能够看到UsesGenericParameter函数的C++实现:








      extern "C" Object_t * GenericType_1_UsesGenericParameter_m10449_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method){ { Object_t * L_0 = ___value; return L_0; }}
      在两处使用泛型参数T的地方(分别在返回值和函数参数中),C++代码都使用了Object_t*。因为任何引用类型都能在C++代码中被Object_t*所表示,所以我们也就能够对于任何引用T,调用相同的UsesGenericParameter函数。
      在系列的第二篇中,我们有提到过在C++代码中,所有的函数都是非成员函数。il2cpp.exe不会因为在C#有重载函数而在C++中使用继承。在是在类型的处理上却有所不同:il2cpp.exe确实会在类型的处理上使用继承。如果我们查找代表C#中AnyClass类的C++类型AnyClass_t,会发现如下代码:




      struct AnyClass_t1 : public Object_t{};
      因为AnyClass_t1是从Object_t继承而来,我们就能合法的传递一个 AnyClass_t1的指针给GenericType_1_UsesGenericParameter_m10449_gshared函数。
      那函数的返回值又是个什么情况呢?如果函数需要返回一个继承类的指针,那我们就不能返回它的基类对吧。那就让我们看看GenericType::UsesGenericParameter的声明:


      #define GenericType_1_UsesGenericParameter_m10452(__this, ___value, method) (( AnyClass_t1 * (*) (GenericType_1_t6 *, AnyClass_t1 *, MethodInfo*))GenericType_1_UsesGenericParameter_m10449_gshared)(__this, ___value, method)
      C++代码其实是把返回值(Object_t*类型)强制转换成了AnyClass_t1*类型。因此在这里IL2CPP对C++编译器使了个障眼法。因为C#的编译器会保证UsesGenericParameter中的T是可兼容的类型,因此IL2CPP这里的强转是安全的。
      带泛型约束的共享
      假设如果我们想要让T能够调用一些特定的函数。因为System.Object只有最基本的一些函数而不存在你想要使用的任何其他函数,那么在C++中使用Object_t*就会造成障碍了,不是嘛?是的,你说的没错!但是我们有必要在此解释一下C#编译器中的泛型约束的概念。
      让我们再仔细看看InterfaceConstrainedGenericType的C#代码。这个泛型类型使用了一个‘where’关键字以确保T都是从一个特定的接口(Interface):AnswerFinderInterface继承过来的。这就使得调用ComputeAnswer 函数成为可能。大家还记得上一篇博文中我们讨论的吗:当调用一个接口函数的时候,我们需要在虚表(vtable structure)中进行查找。因为FindTheAnswer可以从约束类型T中被直接调用,所以C++代码依然能够使用全共享的实现机制,也就是说T由Object_t*所代表。
      如果我们由HelloWorld_DemonstrateGenericSharing_m4function的实现开始,跳到InterfaceConstrainedGenericType_1__ctor_m11函数的定义,会发现这个函数任然是一个#define定义,映射到了InterfaceConstrainedGenericType_1__ctor_m10456_gshared函数。在这个函数下面,是InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared函数的实现,发现它也是一个全共享函数,接受一个Object_t*参数,然后调用InterfaceFuncInvoker0::Invoke函数转而调用实际的ComputeAnswer代码。














      extern "C" int32_t InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared (InterfaceConstrainedGenericType_1_t2160 * __this, Object_t * ___experiment, MethodInfo* method){ static bool s_Il2CppMethodIntialized; if (!s_Il2CppMethodIntialized) { AnswerFinderInterface_t11_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&AnswerFinderInterface_t11_0_0_0); s_Il2CppMethodIntialized = true; } { int32_t L_0 = (int32_t)InterfaceFuncInvoker0::Invoke(0 /* System.Int32 HelloWorld/AnswerFinderInterface::ComputeAnswer() */, AnswerFinderInterface_t11_il2cpp_TypeInfo_var, (Object_t *)(*(&___experiment))); return L_0; }}
      因为IL2CPP把所有的C#中的接口(Interface)都当作System.Object一样处理,其所产生的C++代码也就能说得通了。这个规则在C++代码的其他情况中也同样适用。
      基类的约束
      除了对接口(Interface)进行约束,C#还允许对基类进行约束。IL2CPP并不是把所有的基类都当成System.Object处理。那么对于有基类约束的泛型共享又是怎样的呢?
      因为基类肯定都是引用类型,所以IL2CPP还是使用全共享版本的泛型函数来处理这些受约束的类型。任何有用到约束类型中特定成员变量或者成员函数的地方都会被C++代码进行强制类型转换。再次强调,在这里我们仰仗C#编译器强制检查这些约束类型都符合转换要求,我们就可以放心的蒙蔽C++编译器了。
      值类型的泛型共享
      让我们回到HelloWorld_DemonstrateGenericSharing_m4函数看下 GenericType的实现。DateTime是个值类型,因此GenericType不会被共享。我们可以看看这个类型的构造函数GenericType_1__ctor_m10。这个函数是GenericType所特有的,不会被其他类使用。
      系统的思考泛型共享
      泛型共享的实现是比较难以理解的,问题的本身在于它自己充满着各种不同的特殊情况(比如:奇特的递归模板模式)(译注:这是C++中的一个概念,简单的说就是诸如:class derived:public base这样的形式,使用派生类本身来作为模板参数的特化基类。目的是在编译期通过基类模板参数来得到派生类的行为,由于是编译期绑定而不是运行期绑定,可以增加执行效率)。
      从以下几点着手可以帮助我们很好的思考泛型共享:

      泛型类中的函数都是共享的有些泛型类只和他们自己共享代码(比如泛型参数是值的泛型类)泛型参数是引用的泛型类总是全共享-他们总是使用System.Object来适用于各种参数类型有两个或者更多泛型参数的泛型类能够被部分共享。前提是在泛型参数中至少有一个参数是引用类型il2cpp.exe总是先产生全共享代码。其他特别的代码在有用到时才会特别单独产生。
      泛型函数的共享
      泛型类可以被共享,泛型函数同样也可以。在我们原始的C#示例代码中,有一个UsesDifferentGenericParameter函数,这个函数用了另外一个泛型参数而不是GenericType。我们在GenericType类的C++代码中查找不到UsesDifferentGenericParameter的实现。事实上,它在GenericMethods0.cpp中:








      extern "C" Object_t * GenericType_1_UsesDifferentGenericParameter_TisObject_t_m15243_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method){ { Object_t * L_0 = ___value; return L_0; }}
      请注意这个是一个泛型函数的全共享版本,因为它接受Object_t*作为参数。虽然这是一个泛型函数,但是它的行为在非泛型的情况下是一样的。il2cpp.exe总是试图先产生一个使用泛型参数的实现。
      结论
      泛型共享是自IL2CPP发布以来一个最重要的改进。通过共享相同的代码实现,它使得C++代码尽可能的小。我们也会继续利用共享代码机制来进步一减少最终二进制文件的尺寸。
      在下一篇文章中,我们将探讨 p/invoke 封装代码是如何产生的。以及托管代码中的类型数据是如何转换到原生代码(C++代码)中的。我们将检视各种类型转换所需要的开销,并且尝试调试有问题的数据转换代码。


      原文连接



    • ayame9joe 1485 3

      类似《迷失之风》的物理效果实现?

      滑动手指控制主角移动。
      是在接触点上施加一个作用力?
      求一个实现思路。

    • 红茶君 1748 1

      昨天给莎木3投完钱,今天来看看E3上的独立游戏

      这几天E3的消息铺天盖地,我知道你们都被hololens版的Minecraft、FF7重制、莎木3等等各种重磅炸弹刷屏了。然而我们这个业界的繁荣,不光是因为巨头们费尽心思的准备大型武器,也依靠无数小团队的奇妙创意。
      所以,让我们来看看本次E3发布会上一闪而过的独立游戏们。
      首先是连续几年登上索尼发布会的太空沙盒游戏No Man’s Sky(无人深空),这个看起来规模无比庞大的宇宙探索游戏,却是由来自英国的小团队Hello Games制作的。从开发商公布的最新消息来看,这个通过算法生成宇宙的游戏,拥有着天文数字级别的星球数量,以及难以穷尽的广袤宇宙。
      以下是E3索尼发布会上最新的游戏演示。如果开发团队的设想能够成功实现,那么这可能会一个独立团队做过的最黑科技的事了。
      而微软则在发布会上专门设立了一个ID@Xbox的单元,公布了一系列将要登上Xbox的独立游戏作品,视频如下。视频里出现了好几个值得注意的新作品:Cuphead,上世纪30年代动画片风格的新作;Tacoma,由获奖无数的Gone Home开发团队开发的新作品,看起来对游戏叙事又有了新的探索;Ashen,一个风格和氛围都很棒的开放世界RPG游戏。除此之外,我们之前报道过的Gemini也在这个视频剪辑中出现了(查看对Gemini团队的采访可以关注IndieACE微信公众号,然后回复gemini取得)。

      剪辑中出现的所有游戏名单如下,一些游戏已经发售或者处于Early Access阶段。大部分游戏都将可以在Xbox One或者PC上玩到。
      Cuphead(Studio MDHR)Beyond Eyes(team17,Tiger & Squid)Rise & Shine(Adult Swim Games)Gemini(Echostone Games)Below(Capybara Games)The Flame in the Flood(The Molasses Flood)Ashen(Aurora44)The Solus Project(Grip Games)Westerado: Double Barreled (Adult Swim Games),已发售Happy Dungeons(Toylogic)Recruits(QUByte Interactive),Early Access on SteamCastle Crashers Remastered(The Behemoth)Tacoma(Fullbright)Full Mojo(Nicalis),已发售Zheros(Rimlight Studios)Sword Coast Legends(n-Space and Digital Extremes)Goat Simulator: Mmore Goatz Edition(Double Eleven)Game 4(The Behemoth)Phantasmal(Eyemobi),Early Access on SteamWarhammer(Fatshark)Outward(Nine Dots Studio)Sheltered(team17,Unicube)ARK: Survival Evolved(Studio Wildcard),Early Access on SteamFishing(Dovetail Games),Early Access on SteamSpace Dust Racers(Space Dust Studios)The Mean Greens(Virtual Basement)The Long Dark(Hinterland Games),Early Access on SteamSuperhot(Superhot)

      感谢这个好游戏多得玩不过来的时代。

    • Sobek 3000 3

      看了眼E3,有一个像素风格的游戏

      游戏名字是"Eitr",去年11月在steam青睐之光第一眼看就表示关注,近年上了E3 PC站台。像素风格,Diablo+Dark Souls的2D结合体
      最后今天才发现作者用的是U3D做的{:235_554:}
      EITR: 5 TOOLS BEHIND PIXEL-ART ACTION/RPG EITR
      这个是主页:http://eitrthegame.com/ 视频需要翻墙。




      刚开始做游戏,之前在用COCOS做了一些简单游戏的demo,感觉有些局限了,现在想转U3D看看能否更简单的做一些有趣的游戏,这种类型的第一眼就爱上它了



    • Bowie 2005

      IL2CPP 深入讲解:方法调用介绍

      IL2CPP深入讲解:方法调用介绍IL2CPP INTERNALS: METHOD CALLS


      这里是本系列的第四篇博文。在这篇文章里,我们将看到il2cpp.exe如何为托管代码中的各种函数调用生成C++代码。我们在这里会着重的分析6种不同类型的函数调用:
      类实例的成员函数调用和类的静态函数调用编译期生成的代理函数调用虚函数调用C#接口(Interface)函数调用运行期决定的代理函数调用通过反射机制的函数调用
      对于每种情况,我们主要探讨两点:相应的C++代码都做了些啥以及这么做的开销如何。和以往的文章一样,我们这里所讨论的代码,很可能在新的Unity版本中已经发生了变化。尽管如此,文章所阐述的基本概念是不会变的。而文章中关于代码的部分都是属于实现细节。

      项目设置这次我采用的Unity版本是5.0.1p4。运行环境为Windows,目标平台选择了WebGL。同样的,在构建设置中勾选了“Development Player”并且将“Enable Exceptions”选项设置成“Full”。我将使用一个在上一篇文章中的C#代码,做一点小的修改,放入不同类型的调用方法。代码以一个接口(Interface)定义和类的定义开始:[code]interface Interface {
      int MethodOnInterface(string question);
      }

      class Important : Interface {
      public int Method(string question) { return 42; }
      public int MethodOnInterface(string question) { return 42; }
      public static int StaticMethod(string question) { return 42; }
      }[/code]
      接下来是后面代码要用到的常数变量和代理类型:[code]private const string question = "What is the answer to the ultimate question of life, the universe, and everything?";

      private delegate int ImportantMethodDelegate(string question);[/code]


      最后是我们讨论的主题:6种不同的函数调用的代码(以及必须要有的启动函数,启动函数具体代码就不放上来了):[code]private void CallDirectly() {
      var important = ImportantFactory();
      important.Method(question);
      }

      private void CallStaticMethodDirectly() {
      Important.StaticMethod(question);
      }

      private void CallViaDelegate() {
      var important = ImportantFactory();
      ImportantMethodDelegate indirect = important.Method;
      indirect(question);
      }

      private void CallViaRuntimeDelegate() {
      var important = ImportantFactory();
      var runtimeDelegate = Delegate.CreateDelegate(typeof (ImportantMethodDelegate), important, "Method");
      runtimeDelegate.DynamicInvoke(question);
      }

      private void CallViaInterface() {
      Interface importantViaInterface = new Important();
      importantViaInterface.MethodOnInterface(question);
      }

      private void CallViaReflection() {
      var important = ImportantFactory();
      var methodInfo = typeof(Important).GetMethod("Method");
      methodInfo.Invoke(important, new object[] {question});
      }

      private static Important ImportantFactory() {
      var important = new Important();
      return important;
      }

      void Start () {}[/code]


      有了这些以后,我们就可以开始了。还记得所有生成的C++代码都会临时存放在Temp\StagingArea\Data\il2cppOutput目录下么?(只要Unity Editor保持打开)别忘了你也可以使用 Ctags 去标注这些代码,让阅读变得更容易。

      直接函数调用最简单(当然也是最快速)调用函数的方式,就是直接调用。下面是CallDirectly方法的C++实现:[code]Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
      V_0 = L_0;
      Important_t1 * L_1 = V_0;
      NullCheck(L_1);
      Important_Method_m1(L_1, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_Method_m1_MethodInfo);[/code]


      代码的最后一行是实际的函数调用。其实没有什么特别的地方,就是一个普通的C++全局函数调用而已。大家是否还记得“代码生成之旅”文章中提到的内容:il2cpp.exe产生的C++代码的函数全部是类C形式的全局函数,这些函数不是虚函数也不是属于任何类的成员函数。接下来,直接静态函数的调用和前面的处理很相似。下面是静态函数CallStaticMethodDirectly的C++代码:[code]Important_StaticMethod_m3(NULL /*static, unused*/, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_StaticMethod_m3_MethodInfo);[/code]


      相比之前,我们可以说静态函数的代码处理要简单的多,因为我们不需要类的实例,所以我们也不需要创建实例,进行实例检查的那些个代码。静态函数的调用和一般函数调用的区别仅仅在于第一个参数:静态函数的第一个参数永远是NULL。由于这两类函数的区别是如此之小,因此在后面的文章中,我们只会拿一般函数调用来讨论。但是这些讨论的内容同样适用于静态函数。

      编译期代理函数调用像这种通过代理函数来进行非直接调用的稍微复杂点的情况会发生什么呢?CallViaDelegate函数调用的C++代码如下:[code]
      // Get the object instance used to call the method.
      Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
      V_0 = L_0;
      Important_t1 * L_1 = V_0;

      // Create the delegate.
      IntPtr_t L_2 = { &Important_Method_m1_MethodInfo };
      ImportantMethodDelegate_t4 * L_3 = (ImportantMethodDelegate_t4 *)il2cpp_codegen_object_new (InitializedTypeInfo(&ImportantMethodDelegate_t4_il2cpp_TypeInfo));
      ImportantMethodDelegate__ctor_m4(L_3, L_1, L_2, /*hidden argument*/&ImportantMethodDelegate__ctor_m4_MethodInfo);
      V_1 = L_3;
      ImportantMethodDelegate_t4 * L_4 = V_1;

      // Call the method
      NullCheck(L_4);
      VirtFuncInvoker1::Invoke(&ImportantMethodDelegate_Invoke_m5_MethodInfo, L_4, (String_t*) &_stringLiteral1);[/code]


      我加入了一些注释以标明上面代码的不同部分。需要注意的是实际上在C++中调用的是VirtFuncInvoker1::Invoke这个函数。此函数位于GeneratedVirtualInvokers.h头文件中。它不是由我们写的IL代码生成的,相反的,il2cpp.exe是根据虚函数是否有返回值,和虚函数的参数个数来生成这个函数的。(译注:VirtFuncInvokerN是表示有N个参数有返回值的虚函数调用,而VirtActionInvokerN 则表示有N个参数但是没有返回值的虚函数调用,上面的例子中VirtFuncInvoker1::Invoke的第一个模板参数int32_t就是函数的返回值,而VirtFuncInvoker1中的1表示此函数还有一个参数,也就是模板参数中的第二个参数:String_t*。因此可以推断VirtFuncInvoker2应该是类似这样的形式:VirtFuncInvoker2::Invoke,其中R是返回值,S,T是两个参数)具体的Invoke函数看起来是下面这个样子的:[code]template
      struct VirtFuncInvoker1
      {
      typedef R (*Func)(void*, T1, MethodInfo*);

      static inline R Invoke (MethodInfo* method, void* obj, T1 p1)
      {
      VirtualInvokeData data = il2cpp::vm::Runtime::GetVirtualInvokeData (method, obj);
      return ((Func)data.methodInfo->method)(data.target, p1, data.methodInfo);
      }
      };[/code]


      libil2cpp中的GetVirtualInvokeData函数实际上是在一个虚函数表的结构中寻找对应的虚函数。而这个虚函数表是根据C#托管代码建立的。在找到了这个虚函数后,代码就直接调用它,传入需要的参数,从而完成了函数调用过程。你可能会问,为什么我们不用C++11标准中的可变参数模板 (译注:所谓可变参数模板是诸如template,这样的形式,后面的...和函数中的可变参数...作用是一样的)来实现这些个VirtFuncInvokerN函数?这恰恰是可变参数模板能解决的问题啊。然而,考虑到由il2cpp.exe生成的C++代码要在各个平台的C++编译器中进行编译,而不是所有的编译器都支持C++11标准。所以我们再三权衡,没有使用这项技术。那么虚函数调用又是怎么回事?我们调用的不是C#类里面的一个普通函数吗?大家回想下上面的代码:我们实际上是通过一个代理方法来调用类中的函数的。再来看看上面的C++代码,实际的函数调用是通过传递一个MethodInfo*结构(函数元信息结构):ImportantMethodDelegate_Invoke_m5_MethodInfo作为参数来完成的。再进一步看ImportantMethodDelegate_Invoke_m5_MethodInfo中的内容,会发现它实际上调用的是C#代码中ImportantMethodDelegate类型的Invoke函数(译注:也就是C#代理函数类型的Invoke函数)。而这个Invoke函数是个虚函数,所以最终我们也是以虚函数的方式调用的。Wow,这够我们消化一阵子的了。在C#中的一点小小的改变,在我们的C++代码中从简单的函数调用变成了一系列的复杂函数调用,这中间还牵扯到了查找虚函数表。显然通过代理的方法调用比直接函数调用更耗时。还有一点需要注意的是在代理方法调用处理时候使用的这个查找虚函数表的操作,也同样适用于虚函数调用。

      接口方法调用在C#中通过接口方法调用当然也是可以的。在C++代码实现中和虚函数的处理方式差不多:[code]Important_t1 * L_0 = (Important_t1 *)il2cpp_codegen_object_new (InitializedTypeInfo(&Important_t1_il2cpp_TypeInfo));
      Important__ctor_m0(L_0, /*hidden argument*/&Important__ctor_m0_MethodInfo);
      V_0 = L_0;
      Object_t * L_1 = V_0;
      NullCheck(L_1);
      InterfaceFuncInvoker1::Invoke(&Interface_MethodOnInterface_m22_MethodInfo, L_1, (String_t*) &_stringLiteral1);[/code]


      实际上的函数调用是通过InterfaceFuncInvoker1::Invoke来完成的。这个函数存在于GeneratedInterfaceInvokers.h头文件中。就像上面提到过的VirtFuncInvoker1类,InterfaceFuncInvoker1类也是通过il2cpp::vm::Runtime::GetInterfaceInvokeData查询虚函数表来确定实际调用的函数的。
      那为什么接口的方法调用和虚函数的调用在libil2cpp库中是不同的API呢?那是因为在接口方法调用中,除了方法本身的元信息,函数参数之外,我们还需要接口本身(在上面的例子中就是L_1)在虚函数表中接口的方法是被放在一个特定的偏移上的。因此il2cpp.exe需要接口的信息去计算出被调用的函数到底是哪一个。
      从代码的最后一行可以看出,调用接口的方法和调用虚函数的开销在IL2CPP中是一样的。

      运行期决定的代理方法调用使用代理的另一个方法是在运行时由Delegate.CreateDelegate动态的创建代理实例。这个过程实际上和编译期的代理很像,只是多了一些运行时的处理。为了代码的灵活性,我们总是要付出些代价的。下面是实际的代码:[code]// Get the object instance used to call the method.
      Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
      V_0 = L_0;

      // Create the delegate.
      IL2CPP_RUNTIME_CLASS_INIT(InitializedTypeInfo(&Type_t_il2cpp_TypeInfo));
      Type_t * L_1 = Type_GetTypeFromHandle_m19(NULL /*static, unused*/, LoadTypeToken(&ImportantMethodDelegate_t4_0_0_0), /*hidden argument*/&Type_GetTypeFromHandle_m19_MethodInfo);
      Important_t1 * L_2 = V_0;
      Delegate_t12 * L_3 = Delegate_CreateDelegate_m20(NULL /*static, unused*/, L_1, L_2, (String_t*) &_stringLiteral2, /*hidden argument*/&Delegate_CreateDelegate_m20_MethodInfo);
      V_1 = L_3;
      Delegate_t12 * L_4 = V_1;

      // Call the method
      ObjectU5BU5D_t9* L_5 = ((ObjectU5BU5D_t9*)SZArrayNew(ObjectU5BU5D_t9_il2cpp_TypeInfo_var, 1));
      NullCheck(L_5);
      IL2CPP_ARRAY_BOUNDS_CHECK(L_5, 0);
      ArrayElementTypeCheck (L_5, (String_t*) &_stringLiteral1);
      *((Object_t **)(Object_t **)SZArrayLdElema(L_5, 0)) = (Object_t *)(String_t*) &_stringLiteral1;
      NullCheck(L_4);
      Delegate_DynamicInvoke_m21(L_4, L_5, /*hidden argument*/&Delegate_DynamicInvoke_m21_MethodInfo);
      [/code]

      首先我们使用了一些代码来创建代理这个实例,随后处理函数调用的代码也不少。在后面的过程中我们先创建了一个数组用来存放被调用函数的参数。然后调用代理实例中的DynamicInvoke方法。如果我们更深入的研究下DynamicInvoke方法,会发现它实际上在内部调用了VirtFuncInvoker1::Invoke函数,就如同编译期代理所做的那样。所以从代码执行量上来说,运行时代理方法比静态编译代理方法多了一个函数创建,比且还多了一次虚函数表的查找。

      通过反射机制进行方法调用毫无疑问的,通过反射来调用函数开销是最大的。下面我们来看看具体的CallViaReflection函数所生成的C++代码:[code]// Get the object instance used to call the method.
      Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
      V_0 = L_0;

      // Get the method metadata from the type via reflection.
      IL2CPP_RUNTIME_CLASS_INIT(InitializedTypeInfo(&Type_t_il2cpp_TypeInfo));
      Type_t * L_1 = Type_GetTypeFromHandle_m19(NULL /*static, unused*/, LoadTypeToken(&Important_t1_0_0_0), /*hidden argument*/&Type_GetTypeFromHandle_m19_MethodInfo);
      NullCheck(L_1);
      MethodInfo_t * L_2 = (MethodInfo_t *)VirtFuncInvoker1::Invoke(&Type_GetMethod_m23_MethodInfo, L_1, (String_t*) &_stringLiteral2);
      V_1 = L_2;
      MethodInfo_t * L_3 = V_1;

      // Call the method.
      Important_t1 * L_4 = V_0;
      ObjectU5BU5D_t9* L_5 = ((ObjectU5BU5D_t9*)SZArrayNew(ObjectU5BU5D_t9_il2cpp_TypeInfo_var, 1));
      NullCheck(L_5);
      IL2CPP_ARRAY_BOUNDS_CHECK(L_5, 0);
      ArrayElementTypeCheck (L_5, (String_t*) &_stringLiteral1);
      *((Object_t **)(Object_t **)SZArrayLdElema(L_5, 0)) = (Object_t *)(String_t*) &_stringLiteral1;
      NullCheck(L_3);
      VirtFuncInvoker2::Invoke(&MethodBase_Invoke_m24_MethodInfo, L_3, L_4, L_5);[/code]


      就和运行时代理方法调用一样,我们需要用额外的代码创建函数参数数组。然后还需要调用一个MethodBase::Invoke (实际上是MethodBase_Invoke_m24函数)虚函数,由这个函数调用另外一个虚函数,在能最终得到实际的函数调用!

      总结虽然Unity没有针对C++函数调用的性能分析器,但是我们可以从C++的源码中看出不同类型方法调用的不同复杂程度的实现。如何可能,请尽量避免使用运行时代理方法和反射机制方法的调用。当然,想要提高运行效率还是要在项目的早期阶段就使用性能分析器进行诊断。
      我们也在一直想办法优化il2cpp.exe产生的代码。因此再次强调,这篇文章中所产生的C++代码或许会在以后的Unity版本中发生变化。
      下篇文章我们将更进一步的深入到函数中,看看我们是如何共享方法简化C++代码并减小最终可执行文件的尺寸的。
      原文连接


    • 红茶君 1824 1

      为信仰充值要花多少钱:独立游戏应该如何定价?

      导语:不久之前,Steam开始实行一个游戏退货制度:玩家在购买游戏的两周内,如果对游戏不满意,而游戏时间也未达到2小时,可以申请退款。这个政策在开发者中间引起不少争议。但从另一些角度看,也未必是一件坏事。有太多人体验过在Steam疯狂打折时非理性地买买买,然后在面对一堆自己不会去玩的游戏时后悔不已。希望Steam做出的改变可以让玩家更加谨慎地考虑一个游戏在他们心中究竟价值几何,而不是在打折季中沦陷。
      这篇文章出自Axiom Verge的开发者Dan Adelman,他早就开始反思各平台疯狂的折扣和大量的低价游戏对开发者的影响,并写出了一些很有意思的定价建议。他的观点和立场颇有一些古典经济学的风骨:游戏定价的关键,在于正确预估你的游戏对于玩家的价值。
      希望他对Axiom Verge的定价和宣传思路能给各位游戏开发者一些启发。[indent]文章:On Indie Game Pricing
      作者:Dan Adelman[/indent]
      本周早些时候,我们宣布了发布Axiom Verge的消息。我和Tom为这款作品工作了6个月,我非常激动,所以我更能想象为它独自工作了5年的Tom此时激动的心情。大部分玩家给出的反馈都不错,但有些人质疑它19.99美元的售价。我写这篇博客的原因不是为了证明这个售价的合理性,而是为了阐释游戏定价背后的逻辑,因为这几年有许多游戏开发者向我咨询过该如何对游戏定价。
      我们应该如何定价……理论上

      经济学101的定价方法是十分简单直接的。你有一群潜在的买家,每个人都会对你的游戏价值进行一个独立的评估,如果你能完美掌握每个人心中最大的价格限度这个信息,那么你就能挑一个合适的价格点,使收入最大化(价格x销售量,价格上升,销售量就会下降,反之亦然),然后你就可以盈利。由于电子游戏的发售渠道是数字分发,所以一旦游戏完成,增加收入和增加盈利就成了一回事,因为成本已经固定了。多一份拷贝并没有真正使成本增加。
      如果人们心中愿意支付的最大价格高于你设置的价格,那么就皆大欢喜。如果人们的支付意愿等于你设置的价格,那么对他们来说,买不买你的游戏就没有两样。一些开发者和发行商试图在这其中保持平衡。对于那些支付意愿非常高的人,你可以提供像季卡、DLC或是其他产生高利润的道具去获取额外的收入。但我个人认为这件事很难做得非常优雅,虽然育碧、EA和Zynga对这种做法并不避讳。
      注意,在这种理论模型中,定价不是基于开发成本的。一直有一种说法,说AAA游戏应该卖得比独立游戏贵,因为它们的开发成本高。只需要稍加思考,就会明白这种说法毫无道理。首先,如果一个游戏定价太高,购买者的数量就会下降——这和你的开发成本无关。许多人认为高定价意味着能赚更多钱,所以定价高的开发商就是贪婪。
      第二,为什么玩家要关心一款游戏是多少人做出来的呢?如果我可以用魔杖在顷刻之间毫不费力地做出与Skyrim或者GTA:V同等规模与质量的作品,游戏体验会因此减少分毫吗?如果我花2000万美金做出了个垃圾app,难道它就更值得花钱吗?不,绝不是这样的。
      理论世界的难题
      经济学可以让我们理解商业决策的广泛方式,因为它不会纠结于细节。然而,当将其运用于真实世界时,我们就需要考虑其他因素的影响。顺便说一句,这不仅限于经济学这一门知识。
      最大的问题之一就是,游戏是一种体验式的商品,这意味着在人们体验到它之前,它的价值对他们来说并不重要。电影和书籍也是一样。消费者试图通过专家的评论、朋友的推荐、试玩、用户评分等等方式来解决这个问题,但他们都是不完美的解决方案。
      开发商和发行商认为,人们的最大支付意愿是一个非理性的价格(意味着他们的期待值过高),所以他们希望能不过多透露游戏信息,以尽量长久地保持这种期待。这就是发行商们在游戏发布前、甚至有时在发布或者预售后,都不愿放出游戏评论的原因。而通常的做法是,在游戏通过认证后就尽早发布。我们正在与Sony一起努力,确保评论家们差不多能提前一个月拿到游戏。
      想象一下,你在看一场棒球赛。到了比赛的关键时刻,所以你站起来以便看得更清楚,这很合理,这最大化了你的个人利益。但是你身后的人就有了两个选择:坐着看不见、或是站着看得更清楚。于是,为了最大化个人利益,他们选择站立,以此类推。最后,每个人都站起来了,没有人比之前看得更清楚,每个人只是更不舒服。
      同样的问题也发生在促销活动中。当销售量下降时,合理的方法是降低售价。很多游戏用这个方法赚了很多钱——实际上是赚了大部分钱。2011年Gabe Newell把他们的游戏售价降低了75%,他们的收入增加了40倍!
      但是,如果每个人都这么做,这最终将是一个通往底部的竞赛。玩家被训练成不会以全价购买游戏,像每个人一样,我对此充满罪恶感。通过捆绑销售模式,我在Steam上买了超过200个游戏,许多游戏我都从未玩过。(实际上有很多我都不知道我居然拥有它。我记得我在PAX上看到一个很酷的游戏,回到家我打算买下它,但我发现我早已经拥有它了。)现在有了PS Plus,我在PS4和PS Vita上又有了一大堆我并不真的想玩的游戏。而在App Store上,人们则在一堆0.99美元的游戏中眼花缭乱。
      有人会说,一个游戏从我身上获得收入总好过什么都没有,这个说法看起来似乎公平。但问题在哪呢?如果我们把销售看出是一个如同天上出现蓝月亮一般的偶然事件,那么游戏开发能获得可持续收入的根基也就不存在了。
      顺带一说,玩家会为了短期的自我利益而去特意等待打折。他们可以搭上那些付了全款的人的顺风车(或者是那些不能自给自足的开发者们),付一小部分的价格拿到同样的商品。不幸的是,这对玩家群体来说不是好事。如果玩家们坚持在折扣很多的时候买游戏,那么最终游戏的质量会受影响。看看App Store就知道了。
      除了最大化短期个人利益的理由之外,玩家等待促销也有别的原因。因为游戏是一种体验商品,在玩到它之前没有人能够确定它的价值,所以尽早玩到是有意义的。我想我们都曾经有过这样的经历:原以为一个游戏超级棒,但是打开之后只玩了5分钟。除非我们能有一种体面的方式能让玩家在玩过游戏之后调整支付价格,否则对玩家来说,这个问题的唯一解决方案就是等。
      我曾经想过一些改善的方法。或许是在完成游戏后的虚拟消费的打赏机制?或许是提供更多的购买商品?也可能是换一个思路——付全价然后允许玩家退款,只要他的游戏时长没有超出一定限度。(基本上相当于一个预付费的demo)没有哪个解决方案是完美的,但是开发者、平台和玩家需要认真地考虑这些问题,如果我们不希望被垃圾的海洋所淹没的话。
      个体的利益最大化诉求和群体的利益存在着一些不连贯,如果我们可以共谋并达成协议,规定每个人应当如何为游戏付费,我们会过得更好。幸运(或者说是不幸?)的是,这是不可能的。相反,我只是恳请开发者们认真考虑自己游戏的价值并有所坚持,然后给游戏一个合理的定价。而玩家们需要考虑这些游戏是否真的值得。不是考虑现行的折扣,而是考虑它们到底值多少。
      经济学理论描述了两种价格设置的主要模型。第一种正如我上文说过的,当你的产品非常优秀,市面上不存在真正的替代品,那么如果人们想要得到它,他们就只能付出你所设定的价格。另一种则是商品价格的模型,种小麦的农民无法到市场上去说服每个人,他的小麦比别人的好。他无法为他的小麦设定价格,他只能在当天的市场上看到如今的价格是多少。
      所以当你在为你的游戏定价的时候,首先想想你的游戏属于哪一类。你是在卖一样别处没有的东西吗?还是你在贩卖一个与大量的其他游戏相似的作品?如果是后者,你没有其他选择,游戏价格只能由当前市场价格决定。如果你的游戏是前者,那么你得确定你的游戏在读者心中有多重要。如果人们认为Axiom Verge的定价过高,他们一定还是会去玩别的游戏。然而其他游戏也不是Axiom Verge。(我对盗版这个话题避而不谈,那可以写一整篇文章)
      每当我在新游戏发售的新闻下看评论的时候,总会看到有人在评论里说他会等几个月,等到这个游戏打5折的时候再买。同样的情况也出现在PlayStation上,如今人们总是说他们想等到游戏对PS Plus用户免费时再下载。付全价买游戏的人如今基本上成了冤大头。
      我已经说过好些关于我前雇主任天堂的事情(不太好的那些),但我认为这些年来他们做得非常对的一件事就是,他们给玩家一个很明确的信号,就是人们不应该等待游戏降价。如果你想要在Wii U上玩超级马里奥3D世界,你知道它就是59.99美元(重申一下,我在这里忽略了二手游戏市场),这个游戏已经发售一年多了和它的售价没有关系。任天堂是一家能够制作无可替代的作品的厂商,然后他们给出了坚定的信号,他们坚持自己的价值。
      那么,有何建议呢?
      真正解决问题的第一步,其实发生在远比思考定价早得多的时候。你要制作一个必玩的游戏,毫无疑问这是一个所有开发者都强烈希望的,但说起容易做起来难。但是,让我们来假设一下你做到了,第二步该怎么办呢?
      第二步是确保人们知道你的游戏,并且知道它有何独一无二之处。这件事非常不容易,如何做的问题可以写成一整本书。部分工作涉及到你如何向全世界阐述你游戏的愿景,并把这种愿景真的做到你的游戏当中;部分工作则涉及到怎样让更多的人玩到它、并且让他们向自己的朋友推荐这款游戏;而还有部分工作则涉及到在游戏中留给人们足够的想象空间,让人们不会觉得全然了解你的游戏…然而现在,让我们假设你已经做到了全部,接下来呢?
      最后一步分两个方面。首先,设置一个你觉得适当的价格。如果你的游戏(对于那些想买你游戏的玩家来说)比Titanfall, The Order: 1886或者Destiny更好——并且你能说服人们相信这一点——那么定价权就在你手上。不要受到其他独立开发者的影响。(虽然这个问题一直争论不休令人生厌,但我还是要说,如今“独立”这个词已经有了很多的含义。它的所指可以从刚开始学编程的爱好者,一直到涵盖到Double Fine这样的成熟团队。所以你又为什么要在如此多元的范围内给自己设限呢?)
      第二点则有些反直觉。让人们知道你的游戏会在何时打折。我不会建议说永远不要打折出售你的游戏,降价促销有助于重新提起人们对游戏的兴趣,让人们再一次谈论这个游戏。在第二步中,有些曾经被好评稍稍打动过的玩家,会被降价所激励而去购买这个游戏。但这个方法应该有节制地使用,不要让人们感觉到他们应该等到游戏打折再买。而如果你告诉人们你的下一次促销会是什么时候,则会提醒他们做一个决定:这次降价值得等待吗?
      Axiom Verge是怎么做的?
      第一步:质量/差异化:当然我个人一定是怀抱偏见的,但我相信Tom Happ能做出一些让Axiom Verge与众不同的东西,这是一种没有替代品的游戏。但这并非由我来评判,就像我在上面说到的,你必须使出浑身解数保证对游戏感兴趣的评论家们能在发售前拿到一份游戏。如果我们做的是一个PS4版本,那么我们会为他们编一个PC版。我们会让他们对评论暂时保密,直到游戏发售大约一周前再放出。不要隐藏游戏评论,但要保证评论家们有充裕的时间去玩游戏和写评论。(我并不天真,我相信一定有那种玩了5分钟游戏就凭第一印象写评论的人,我希望这些人是少数。)我们要看看他们对游戏质量的看法。
      第二步:将游戏推到众人面前:现在,我们已经竭尽所能,Tom接受了很多很多的媒体采访。我们逐月地对游戏做媒体曝光,这样人们就能慢慢在发售前看到一些游戏的样貌。我们在E3和IndieCade这样的展会上展示游戏,我们带着游戏奔赴各地的活动:GDC、PAX East, 和 SXSW——在这期间,Sony提供了很多帮助:帮我们在PlayStation的博客上建立了一个讨论专区、在零售店里演示游戏等等。如果有一笔大的广告预算,那么我们或许能做到人尽皆知的程度,但如今我们也已经倾尽全力了。
      第三步:我现在已经把Axiom Verge玩了5遍,我觉得19.99美元是一个很合理的价格。(重申一下,可能有偏见!)在降价促销方面,我们暂时还没有公布我们的计划,所以可能我们也可以在这里说一下。索尼刚刚宣布, Axiom Verge将是春季优惠产品中的一个,对PlayStation Plus用户有10%的折扣。我对此感到很高兴,因为这是对一直支持我们的玩家们的回馈。但是在春季优惠的这一周过去后,6个月内我们不会再打折,再之后的促销计划还没有决定。说得更明确一些的话,就是在2015年10月之前,Axiom Verge都不会再降价促销了。
      通过公布这一点,我希望表达以下两件事情:第一,我希望玩家不会觉得他们全价购买游戏是一个错误。第二,我希望那些觉得19.99美元的定价太贵了的人们能够重新考虑一下、能有一个更明智的判断。如果你希望等价格降到一个程度,比如14.99美元,那么你需要衡量一下6个月的等待是否值5美元。对于一些人来说,这是值得等的,而对于另外一些人则不然。我觉得这样很好,我希望玩家能在信息充分的情况下做决定,而不是事后懊悔。因为我们需要消费者有个好的购买体验,对于这一点,我们和玩家是站在一起的。

    • 红茶君 1790

      当一个独立游戏工作室被25亿美元收购以后

      导语:我想很多人都已经看过了今年E3上微软演示hololens版的Minecraft。现在我们也已经知道,去年微软到底是为了什么花25亿美元买下Mojang。
      做独立游戏最童话般的结局是什么?也许Mojang的经历算是一个典型。从一小群人的特立独行开始,到最后的名利双收,绝对是一个可以拍成励志电影的剧本。世界上有几家工作室能有这样的好运呢?
      但是身在其中的人们未必这样想。
      以下是美国《连线》杂志上个月发布的一篇文章,记者写下了去年这笔轰动业界的收购案的另一个侧面——Mojang的员工在收购中究竟经历了什么?对我们大部分人来说,谈论一个25亿美元的收购是奢侈的(毕竟我们没有坐在创业大街的咖啡店里聊天)。但下面这个故事不同于通常的科技媒体对明星团队的报道,它或许能让你感觉熟悉和切近。我们会发现,在很多共同的问题上,独立工作室和商业公司并没有太大的不同,常人的烦恼和欲望、商业化的冲突和矛盾…一样存在于他们的每一个日常。 [indent]The Unlikely Story of Microsoft’s Surprise Minecraft Buyout
      作者:Daniel Goldberg and Linus Larsson原文链接:http://www.wired.com/2015/06/minecraft-book-excerpt/[/indent]
      去年9月,微软花25亿美元收购了Minecraft的开发商Mojang,成为游戏历史上最大的一起收购之一,让无数游戏玩家大吃一惊。这对Mojang的员工来说也是一个巨大的惊喜,纵然这个惊喜并非全然快乐。Daniel Goldberg和Linus Larsson,the definitive history of the game的作者,更新了他们的书卷,写下了Mojang独立时代最后的日子,以下采访的摘录记录了Mojang的员工在收购中经历的事。
      对于Jens Bergensten来说,未来的生活有种令人愉快的熟悉感。虽然Markus相比起初识之时已经改变了许多,但Jens看起来并未因此感到十分担忧。他现在正在负责一个全世界最流行的游戏项目,每天都有成千上万的玩家沉浸在这个游戏世界中。当然,Mojang的盛名也多少波及到他的生活,他会经常受邀到世界各地的大会演讲;有时,人们在街上认出他、和他聊天或索要他的签名。但他日常的举止态度仍然一如往昔,他话不多、心思缜密、有着微妙的幽默感,常常需要人转转脑子才能明白他的笑点。
      2014年的夏天已经接近尾声,Jens感到十分欣慰,因为生活中的一切都在他的掌握之中。
      然而这种感觉很快就会消散殆尽。一天,Mojang的CEO Carl Manneh把他叫住,与他一同进到会议室,让他坐下,并告诉他:三位创始人经过几周的认真考虑,决定要把Mojang卖给微软。这个消息对Jens Bergensten来说,简直是世界上最重磅的炸弹,他起初觉得不真实,当时他能听到Carl同他说话,但他感到无法理解这语言的含义。在他心中,Mojang是独立游戏界中一颗耀眼的明星,公司的创始人曾一遍又一遍地强调,赚钱不是他们的重点。Mojang做游戏是因为做游戏是一件有趣的事,他们有自己独特的行事方式。当然,他们有大笔的收入,但这也是为了不去做那些无聊的工作而存在的,不是吗?为微软工作显然就属于Jens一直想极力避免的无聊之事。
      他与Carl一起坐在会议室里,他只能静静听着。他还记得自己为了极力抛开心中的纠结,尽量避免去想今后的大计划,而是去考虑一些具体的小事:微软会怎么看自己和公司对开源代码的依赖?要如何去和同行介绍自己略显诡异的编程风格?他甚至坚持与Markus私下聊天,他需要确认这次他的朋友是认真的,这不是一个老板为员工精心准备的玩笑。
      Jens还记得他与Markus之后的那次谈话,他们关着门,在一个办公室的玻璃隔间中,沉着而冷静。他们谈到在出售公司的消息公布后,Minecraft的玩家会有何反应:有人会愤怒,有人会觉得遭到了抛弃。Markus会被打上卖掉公司的标签,会成为别人眼中为巨额财富而放弃理想的家伙。他告诉Jens他想远离网络一段时间,或许他只是暂时性地彻底失联,为躲开那些一旦消息公开、一定会找上他的辱骂。
      即将到来的公司出售事件,意味着Jens的工作时间将会被许多突然降临的任务占据。Carl立即就布置给他几项在收购完成前必须做完的任务。他需要回答微软的一些技术问题,并交出自己的代码以便微软检查。后一项是一个软件公司被出售时的标准程序,就像是二手车在被新车主使用前必须经过机械检查。微软需要确保在千万行使Minecraft正常运行的代码中,没有藏着令人不愉快的意外,比如偷偷摸摸的小把戏、或是对未来发展不利的workarounds之类。这意味着微软要检查第三方的一切,以保证他们是可信的。在合同签署、收购款到账之前,Mojang在微软面前将不会保留任何秘密。
      Jens还被严厉地警告过要保守秘密。Carl, Markus,以及 Mojang的第三位联合创始人Jakob Porser必须确保这次的交易不会因为走漏消息而搞砸。对于Jens来说,最痛苦的一点莫过于,他要坐在这间坐满着同事和朋友的办公室里工作,其他人对于这个重磅消息一无所知,而自己却无法告诉他们。他取得了创始人的信任,因而知晓了一个不太愉快的秘密,他非常想把这个秘密与他的朋友们分享,即使他的老板们告诉他务必保密。然而,他还是没有开口说出这个重磅新闻。Jens认为Carl首先告知他这个消息,是因为他需要Jens马上开始代码审查的工作,不然的话,在新闻公布之前,他们很可能也不会告诉他。
      微软深知,自己十有八九是很难留住Mojang员工的信任与忠诚。确保人们保守秘密只是其一,更重要的是,微软和Mojang都需要保证此事不会影响员工们专注于工作、或是使他们在收购宣布时离职。解决办法就是用大笔的钱来摆平——每位在微软收购Mojang后留在公司、并至少工作六个月以上的员工,将得到两百万克朗的奖金,大约相当于30万美金(税后)。换句话说,小笔财富是献给收购案的平安祭。但对于一些人来说,这还是难以阻止他们的离去。
      钱这个东西,和世上的所有事一样,是相对的。Jens并不是Mojang公司中唯一对薪水不满意的员工。实际上,许多人都觉得自己没能从Minecraft的巨大成功中分享应得的好处。当然,Mojang员工的福利已经比大多数人好了。Mojang为大多数员工安排的旅行足以让大多数人嫉妒。比如,在2013年3月份,Minecraft在PC平台和移动平台的销量都超过了一千万,这是两座重要的里程碑。为了庆祝,Markus带着所有的员工坐私人飞机去了摩纳哥。他们在那里开跑车、乘坐直升机、品尝香槟、在豪华游艇上开Party…所有的费用都由公司承担。
      然而,还是有人觉得,他们辛勤工作取得的好处,主要还是由Markus, Jakob和Carl分享了。三位创始人甚至没有和任何其他人分享公司股份,即使是那些从一开始就在Mojang工作的员工。这意味着Minecraft取得的巨大收益还是直接进了他们的口袋,即使Markus已经有两年没有做任何与Minecraft相关的实际工作了。所有员工都领正常工资,再加上Markus在慷慨时发给他们的津贴或是奖金。
      在任何其他公司这都是再正常不过的事。然而Mojang是不同的——或者至少给人一种与众不同的印象。这家公司在人们眼中是由一群团结而随和的朋友组成的,他们很有凝聚力且十分有趣,公司文化真诚而开放,没有谁特别关心谁管理谁的问题。在最初的几年,这些都是真的。但随着公司的成长,公司的气氛也变了,Mojang理想中的美好印象与现实如今分歧不少。管理层和员工之间的距离变远了。许多人不再将Markus,Carl,和Jakob视为与自己平等的、团队中的一员,而是将他们看作是管理人员。Mojang正在渐渐变成一个纯粹的上班场所。
      “管理层很擅长压工资。然而,他们告诉我们Mojang是一个好公司,我们能够免费去参加GDC、所有人都能收到圣诞节奖金”,一名Mojang员工在2014年夏天时对我们说。
      即使如此,人们还是留了下来,几乎没有例外。
      公司出售的新闻改变了Mojang。有人觉得遭到了Markus的背叛,公司士气倍受打击。“感觉像是到了世界末日”,一个在Mojang工作了很久的员工在新闻公布后告诉我们。
      Daniel Rosenfeld,更广为人知的是他的艺名C418,他是为Minecraft制作音乐的作曲家,他是第一个分享对Mojang出售一事看法的Minecraft相关人员。“宣布公司要出售的那天,我觉得我被Markus背叛了”,他在2014年秋天卫报的采访中说,他还补充说他如今能够理解Markus这么做的原因:“Markus只是想一个人做一些没人关心的小游戏。这倒是令我很受用。”
      官方通告只是公布了大家已知的消息,然而有一个重要的细节需要我们注意:Markus, Jakob和Carl三人,在收购完成后全都离职了。没有任何人来告知任何关于Mojang的未来规划或者承诺公司未来将如何发展。
      像Mojang这样的大型收购案,通常的情况下,都会有一些保障性的措施。这可能意味着创始人在收购后的公司董事会里占有席位,他们同意继续担当“顾问”的角色,或是简单地在所有权过渡期继续管理公司。这是为了避免在此期间有太多突然的改变,管理层中熟悉的面孔可以避免短期内员工、用户、合作伙伴中产生过多变数,而导致业务突然崩塌,这保证公司得以走远。然而微软和Mojang之间就像是几乎没有此类协议一样,三位创始人的来去显得高度自由。按常理,微软不太可能对Markus的突然离去不置一词。虽然我们不知道他们之间究竟如何商量,但唯一合理的解释就是,Markus从一开始就强烈要求,自己要在收购完成时就切断与Mojang的所有联系。
      对Markus来说,将公司交予别人并不像是出售后的缺点,反而像是他的全部目的。
      即使把公司卖掉、甩手不管是Markus期盼已久的宣泄,可他淡出众人视线的速度也未免太令人惊讶了。当微软派出代表团来到斯德哥尔摩访问Mojang时,Markus就不在。虽然几乎没人知道确切情况,但仍有一些传言说他前几天才刚刚和Jakob一起从拉斯维加斯回来。无论如何,他要么是疲于见人,要么是没兴趣现于人前。在新主人面前代表Mojang的任务落到了Carl身上。
      上午九点刚过,微软游戏工作室的总经理Matt Booty来到了位于斯德哥尔摩的Mojang总部。他带来了七个代表团成员,他们自我介绍后,Matt就开始讲解他的计划。每个代表Mojang的员工都专注地听着,尽管这个计划中有许多事他们也是初次听闻。
      为什么微软要收购Mojang呢?很简单,因为创始人想把Mojang卖掉。Mojang旗下的卡牌对战游戏《卷轴》(Scrolls)命运将会如何?让我们拭目以待。如此模糊的回答在外人看来似乎争议不大。但实际上,Jakob制作的这款集换式卡牌游戏在此前已经被讨论了很久。它已经拥有一个由几千名玩家组成的粉丝团,这对于一个独立开发的视频游戏来说,是个不错的成绩,但比起出于同门的Minecraft,它交出的却依然是一张失败的成绩单。
      据出席会议的人说,Matt Booty在讨论Mojang的未来时曾几次失言。他数次用Minecraft来代表Mojang整个公司,但很快他就纠正了这个错误。然而对于会议室中的其他人来说,这是尴尬的。他们当中只有不到一半的人直接负责Minecraft的相关工作。每一次这位来自微软的人将公司名字与他们旗下著名游戏的名字弄混时,他都在无意中强调一个众人故意避而不谈、却又显而易见的事实:是的,微软收购了整个Mojang公司,但他们只对Minecraft有兴趣。
      即使如此,Mojang的每位员工还是得到了相当慷慨的对待。此外,微软还向他们保证,今后两年都会按时足量地发放月薪,即使微软今后决定关闭斯德哥尔摩的工作室、将Minecraft的开发业务搬到雷蒙德总部,这一点也不会改变。然而,至少有一位员工拒绝了这份邀请。
      Mojang所有权正式移交的准备手续花了几个星期才完成。当签完所有协议、审查完所有代码,正式移交的日期也就确定了。从2014年11月6日起,Mojang再也不是一个独立的公司。
      所有权移交的前一天,正是Markus在Mojang工作的最后一天。几个同事正在办公,他起身离开。他犹豫了一会儿,不知该如何说再见。于是,他决定不说再见。在他的员工在座位上工作时,他起身走过外面的办公桌、走过放着许多奖杯和荣誉的展示架。他左转出了门、走下一小段楼梯、走出了这栋大楼。大楼的门在他身后关上,十一月寒冷的空气刺痛了他的脸颊。

    • Bowie 3084

      IL2CPP 深入讲解:代码调试之诀窍

      IL2CPP深入讲解:代码调试之诀窍
      IL2CPP INTERNALS : DEBUGGING TIPS FOR GENERATED CODE

      这是IL2CPP深入讲解的第三篇。在这篇中,我们将探索使得调试由IL2CPP生成的C++代码更容易的一些技巧。我们将看到如何设置断点,查看字符串(string)和用户自定义结构中的内容,以及异常断点处理。

      鉴于我们要调试C++代码是由IL2CPP将.Net的IL字节码转换过来的,因此做起来可能不会是一场愉快的经历。然而如果使用本文中说到的这些技巧,我们还是能搞清楚Unity项目中的这些C++代码是如何在目标机器上执行的。(在文章的最后一小段我们还会稍微介绍下调试托管代码的情况)

      和前两篇中提醒大家的一样,如果你的Unity生成的代码和本文的有所不同,请不要奇怪。随着每一个新的Unity版本的发布,我们都在尝试新的方法让生成的C++代码更好,更快,更小。因此产生的代码有所不同也就可以理解了。


      建立项目

      在本篇文章中,我会采用Unity 5.0.1p3,运行环境是OSX。采用的示例项目和前篇博文的一致,只不过这次目标编译平台切换到了iOS并且使用IL2CPP设置。还有一点和前篇博文一样:我勾选了“Development Player”选项,如此一来il2cpp.exe会根据IL代码中的名字来命名C++代码中的类型和函数,使得代码更容易理解。


      在Unity生成了Xcode项目之后,我们将其打开(我用的Xcode版本是6.3.1,不过这个无需一致,只要是近期的版本都行),在Xcode中选择目标设备(我的是iPad Mini 3,但是任何可识别的iOS设备都行)然后构建整个工程。


      设置断点

      在开始运行项目之前,我将在HelloWorld这个类的Start函数中设置一个断点。如同我们在前篇文章中看到的那样,这个函数的名字是HelloWorld_Start_m3。我们能用Cmd+Shift+O组合快捷键,在弹出的菜单里输入函数的名字,然后设置断点。如下图:

      当然,我们也能采用在Xcode菜单中选择Debug > Breakpoints > Create Symbolic Breakpoint来设置断点。


      当我运行程序后,我能立刻看到程序被中断在函数一开始的地方。

      只要我们知道函数的名字,我们可以在任何函数里设置断点。我们也可以在C++文件的特定的一行上设置断点。事实上,所有由IL2CPP产生的文件都在Classes/Native目录中。


      检查字符串

      在Xcode中检查IL2CPP字符串有两种方式。我们可以直接检查字符串的内存,或者我们可以调用一个libil2cpp的字符串辅助函数将其转换成std::string,然后由Xcode显示出来。那就让我们来看看名字叫_stringLiteral1的字符串里面是什么内容吧(剧透慎入!!字符串里面的内容应该是“Hello, IL2CPP!”)

      如果用Ctags 或者在Xcode中使用Cmd+Ctrl+J,我们可以直接跳转到_stringLiteral1的定义所在,发现它是一个Il2CppString_14类型:

      struct Il2CppString_14
      {
      Il2CppDataSegmentString header;
      int32_t length;
      uint16_t chars[15];
      };


      事实上,在IL2CPP中的所有的字符串都有类似的定义。你可以在object-internals.h头文件中找到Il2CppString的定义。这些个字符串首先包含了一个所有托管类型都有的一个结构头:Il2CppObject(在这个特定的例子中,Il2CppObject通过typedef,变成了Il2CppDataSegmentString),接下来跟着一个四字节的长度变量,然后是一个双字节的数组。字符串是在编译期定义的,像_stringLiteral1这样的字符串在编译期就决定了数组的大小,而其他一些字符串是在运行时动态的分配数组的大小。这个数组用的都是UTF-16的编码方式。

      如果我们在Xcode中将_stringLiteral1加入watch window,我们就能够通过“_stringLiteral1”的查看内存选项来窥其一二。


      在内存窗口中,我们看到如下内容:


      字符串一开始是16个字节的头结构,跳过这些之后,我们看到的是四字节的0x000E (14)。在这个长度之后就是字符串的第一个字符了,它的值是0x0048 (‘H’)。由于一个字符是两个字节的宽度(UTF-16编码),而在这个例子中所有的字符都是一个字节表示,所以Xcode在显示的时候在每个字符间加入了额外的点隔开。尽管如此,字符串的内容还是显而易见的。这种查看字符串的方式对于简单的内容来说没有什么问题,但是对于复杂的内容来说,就显得力不从心了。

      我们也可以在Xcode的lldb命令行中查看字符串内容。utils/StringUtils.h头文件中有我们可以使用的libil2cpp的一些辅助函数。我们可以在lldb中使用Utf16ToUtf8函数:

      static std::string Utf16ToUtf8 (const uint16_t* utf16String);

      我们将string结构中的chars成员作为参数传给函数,其会返回一个UTF-8编码方式的std::string。在lldb的命令行中,当我们输入p命令,就能看见字符串的内容了:
      (lldb) p il2cpp::utils::StringUtils::Utf16ToUtf8(_stringLiteral1.chars)
      (std::__1::string) $1 = "Hello, IL2CPP!"


      检视用户自定义类型

      除了字符串,我们也能查看用户自定义类型的内容。在这个项目的代码中,我们创建了一个C#的类型,叫做Important,里面有个成员叫InstanceIdentifier。如果我在创建第二个Important实例的地方设置一个断点,我能如期的看到InstanceIdentifier被设置成了1。


      所以查看用户自定义结构的内容其实和平常在Xcode中查看C++代码的方式没什么两样。

      在代码的异常处设置断点

      调试生成的C++代码的目的多半是为了找出程序中的bug。在许多情况下这些bug可以通过托管的异常机制得以发现,在前篇文章中我们也提到过,IL2CPP使用很多C++的异常来实现托管代码的异常机制,因此在Xcode中我们有几种方式来实现异常的触发。

      最简单的方法是,我们可以在il2cpp_codegen_raise_exception函数中设置断点,当托管代码抛出异常后,il2cpp.exe就会使用这个函数在C++中抛出异常。


      此时如果我让程序继续运行,Xcode就会因为抛出了一个InvalidOperationException异常而停在断点上(译注:首先你得像上图那样设置一个和异常函数相关的断点)。这个时候后刚才讲到的查看字符串的调试方法就显得格外有用了。如果我们深入看下ex参数的内容,能发现其有一个___message_2成员,说明的是这个异常的具体原因。


      只要稍许的提取一下,我们就能将这个异常问题的内容打印出来:
      (lldb) p il2cpp::utils::StringUtils::Utf16ToUtf8(&ex->___message_2->___start_char_1)
      (std::__1::string) $88 = "Don't panic"


      需要注意的是,虽然这里的string结构和前面的一样,但是里面的成员名字有所改变。前面的chars成员现在变成了___start_char_1,而且类型不是uint16_t[]数组而是uint16_t(译注:原文中使用的是uint16_t,但实际上应该是uint16_t*,也就是其指针),其实它还是指向了字符数组的第一个字符,所以我们也还是可以把它传给Utf16ToUtf8函数得到可阅读的字符串。就这个例子而言,其显示的信息(Don't panic)还是令人感到安慰的。

      不是所有的托管异常都会显示的被C++代码抛出。libil2cpp在某些情况下还会抛出不调用il2cpp_codegen_raise_exception函数的异常。我们如何捕获这些异常呢?

      我们可以使用Xcode菜单中的Debug > Breakpoints > Create Exception Breakpoint,创建一个异常断点,然后将类型选择成C++并且设置成Il2CppExceptionWrapper丢出时触发中断。由于C++的这个异常接手所有托管代码异常,所以我们可以在这里发现所有的问题。

      让我们在Start函数中加入如下两行来进行一个验证:

      Important boom = null;
      Debug.Log(boom.InstanceIdentifier);

      代码的第二行会导致一个NullReferenceException异常抛出。当我们在Xcode中运行的时候,我们会看到代码确实停了下来。因为断点是在libil2cpp中,所以我们看到的都是汇编码。没有关系,我们还可以看看线程栈。在线程栈中向前走四个调用,我们会发现NullCheck方法,此方法是il2cpp.exe在代码生成的时候主动注入的。(译注:具体注入的细节可以参考前篇博文)


      由此再上一层,我们就能看到我们的Important实例的值确实是NULL。


      小结

      在讨论了一些调试生成代码的小技巧之后,我希望你在跟踪定位由IL2CPP产生的C++代码问题上有了一个更好的理解。我鼓励你去查看下其他的由IL2CPP产生的类型,熟悉下调试他们的方法。

      那托管代码呢?我们难道不能直接在设备上调试托管代码吗?实际上,这是可能的。我们有一个内部的处于开发阶段的托管代码调试器。这个调试器还没成熟到发布的阶段,但是它在我们的计划之中,如果你感兴趣,不防关注下。

      在下一篇文章中,我们将介绍对于托管代码中不同的函数调用, IL2CPP是如何区别对待实现的。对于每一种实现,我们会着重关注其运行时的开销如何。



      原文连接



    • 君子雷雷 1329

      【indienova】本次 E3 上 IndieCade 展出的部分独立游戏(第一部分)

      更多独立游戏资讯请关注indienova.comIndieCade 今年也高调参加了 E3,并且展出了一些最近比较受关注的独立游戏。今年明显展出移动设备上的游戏比较少,而 Windows、Mac、Linux 以及 PS 平台游戏占大多数,同时还有 VR 类的游戏参加展出。另外还有不少装置类和线下游戏。我们选了一部分值得关注的游戏列在这里,供大家对这次展出的大致情况进行一些了解。根据目前国内关注的情况,对 VR、装置类和线下游戏类做了些删减。IndieCade 展台的与众不同之处在于,开发者就在自己的游戏旁边,可以直接和玩家进行交流和互动,这种机会可是非独立游戏玩家很难得到的,所以现场也是非常的热闹。而且说实话,好多独立游戏不管从题材、立意和执行方面来讲,其实也有超越 3A 大作的方面。也难怪,这次 SONY PlayStation 就是主要的赞助商,他们早就看到独立游戏的潜力,而且知道独立游戏会成为他们的强有力的支柱之一。以下是一些现场的照片: 在《幸运枪手》中,你的运气会影响到你周围的世界,运气好的时候,敌人射向你的子弹会偏转,遇到陷阱会自动规避,同伴鸭子也会给你提供援助;运气不好的时候,桥梁会崩溃,岩石会落到你头上,你的枪会哑火。《幸运枪手》中的幸运是非常有趣的设定,收集金币会让你在各种危险面前安然无恙,还会影响到你装填子弹的质量,但你运气爆表的时候,你的手枪中每一发子弹都会命中敌人(会拐弯);运气不好的时候你需要谨慎前进,任何一个陷阱对你来说都是致命的,非常有意思。(介绍及视频)注:部分展会照片来自 IndieCade tumblr。