l1n6yun's Blog

记录学习的技能和遇到的问题

0%

扩展地址:http://docs.php.net/manual/zh/book.pthreads.php

注意事项

php5.3 或以上,且为线程安全版本。apache 和 php 使用的编译器必须一致。

通过 phpinfo() 查看 Thread Safetyenabled 则为线程安全版。

通过 phpinfo() 查看 Compiler 项可以知道使用的编译器。本人的为:MSVC9 (Visual C++ 2008)。

本人使用环境

32位 windows xp sp3 ,wampserver2.2d(php5.3.10-vc9 + apache2.2.21-vc9)。

一、下载 pthreads 扩展

下载地址:http://windows.php.net/downloads/pecl/releases/pthreads

根据本人环境,我下载的是 pthreads-2.0.8-5.3-ts-vc9-x86 。

2.0.8 代表 pthreads 的版本。
5.3 代表 php 的版本。
ts 表示 php 要线程安全版本的。
vc9 表示 php 要 Visual C++ 2008 编译器编译的。
x86 则表示32位的

二、安装 pthreads 扩展

复制 php_pthreads.dll 到目录 bin\php\ext\ 下面。(本人路径D:\wamp\bin\php\php5.3.10\ext)

复制 pthreadVC2.dll 到目录 bin\php\ 下面。(本人路径D:\wamp\bin\php\php5.3.10)

复制 pthreadVC2.dll 到目录 C:\windows\system32 下面。

打开 php 配置文件 php.ini。在后面加上 extension=php_pthreads.dll

提示!Windows系统需要将 pthreadVC2.dll 所在路径加入到 PATH 环境变量中。

我的电脑—>鼠标右键—>属性—>高级—>环境变量—>系统变量—>找到名称为 Path 的—>编辑—>在变量值最后面加上 pthreadVC2.dll 的完整路径(本人的为C:\WINDOWS\system32\pthreadVC2.dll)。

三、测试 pthreads 扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
class AsyncOperation extends \Thread {
public function __construct($arg){
$this->arg = $arg;
}
public function run(){
if($this->arg){
printf("Hello %s\n", $this->arg);
}
}
}
$thread = new AsyncOperation("World");
if($thread->start())
$thread->join();

运行以上代码出现 Hello World ,说明 pthreads 扩展安装成功!

在网络世界中,安全与攻击技术总是并存的。今天,我们将深入探讨一个名为Low Orbit Ion Cannon(LOIC)的网络攻击工具,它是一种开源的压力测试和拒绝服务(DoS或DDoS)攻击应用程序,用C#编写。本文将详细介绍LOIC的起源、功能、使用方法以及相关的法律风险。

LOIC简介

LOIC 最初由 Praetox Technologies 开发,并后来被发布到公共领域。其源代码现在可以自由获取,并且可以在多个开源平台上下载。LOIC能够执行基本的TCP、UDP或HTTP DoS攻击,当多个用户联合使用时,可以形成DDoS攻击。它的流行部分原因是因为有一个版本与Anonymous组织有关,通过IRC控制通道,允许人们加入自愿的僵尸网络并攻击单一目标。

upload successful

LOIC的工作原理与使用

LOIC 的操作相对简单。用户只需填写目标系统的URL或IP地址,选择攻击方法和端口,然后点击“IMMA CHARGIN MAH LAZER”按钮即可。以下是详细的使用步骤:

  1. 运行工具。
  2. 在相关字段中输入网站的URL或IP,并点击“Lock On”。
  3. 如果你是高级用户,可以更改参数;否则,保持默认设置。
  4. 点击标有“IMMA CHARGIN MAH LAZER”的大按钮。
  5. 攻击开始,你可以在工具中看到攻击状态(例如发送的数据包数量)。

LOIC是否为病毒?

虽然LOIC不是病毒,但许多杀毒软件会将其检测为病毒(类似于trojan.agent/gen-msil flooder),因为它通常被用于恶意目的,并且许多用户在不知情的情况下安装了它。

LOIC的法律风险

使用LOIC进行DoS或DDoS攻击在大多数国家是非法的。因此,我们强烈建议仅在你有权限访问的网络或进行压力测试时使用此工具,以展示DoS攻击的力量。

Why

Copying text to the clipboard shouldn’t be hard. It shouldn’t require dozens of steps to configure or hundreds of KBs to load. But most of all, it shouldn’t depend on Flash or any bloated framework.

That’s why clipboard.js exists.

Install

You can get it on npm.

1
npm install clipboard --save

Or if you’re not into package management, just download a ZIP file.

Setup

First, include the script located on the dist folder or load it from a third-party CDN provider.

1
<script src="dist/clipboard.min.js"></script>

Now, you need to instantiate it by passing a DOM selector, HTML element, or list of HTML elements.

Internally, we need to fetch all elements that matches with your selector and attach event listeners for each one. But guess what? If you have hundreds of matches, this operation can consume a lot of memory.

For this reason we use event delegation which replaces multiple event listeners with just a single listener. After all, #perfmatters.

Usage

We’re living a declarative renaissance, that’s why we decided to take advantage of HTML5 data attributes for better usability.

Copy text from another element

A pretty common use case is to copy content from another element. You can do that by adding a data-clipboard-target attribute in your trigger element.

The value you include on this attribute needs to match another’s element selector.

1
2
3
4
5
6
7
<!-- Target -->
<input id="foo" value="https://github.com/zenorocha/clipboard.js.git">

<!-- Trigger -->
<button class="btn" data-clipboard-target="#foo">
<img src="assets/clippy.svg" alt="Copy to clipboard">
</button>

Cut text from another element

Additionally, you can define a data-clipboard-action attribute to specify if you want to either copy or cut content.

If you omit this attribute, copy will be used by default.

1
2
3
4
5
6
7
<!-- Target -->
<textarea id="bar">Mussum ipsum cacilds...</textarea>

<!-- Trigger -->
<button class="btn" data-clipboard-action="cut" data-clipboard-target="#bar">
Cut to clipboard
</button>

As you may expect, the cut action only works on <input> or <textarea> elements.

Copy text from attribute

Truth is, you don’t even need another element to copy its content from. You can just include a data-clipboard-text attribute in your trigger element.

1
2
3
4
<!-- Trigger -->
<button class="btn" data-clipboard-text="Just because you can doesn't mean you should — clipboard.js">
Copy to clipboard
</button>

Events

There are cases where you’d like to show some user feedback or capture what has been selected after a copy/cut operation.

That’s why we fire custom events such as success and error for you to listen and implement your custom logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var clipboard = new Clipboard('.btn');

clipboard.on('success', function(e) {
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);

e.clearSelection();
});

clipboard.on('error', function(e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
});

Tooltips

Each application has different design needs, that’s why clipboard.js does not include any CSS or built-in tooltip solution.

The tooltips you see on this demo site were built using GitHub’s Primer . You may want to check that out if you’re looking for a similar look and feel.

Advanced Usage

If you don’t want to modify your HTML, there’s a pretty handy imperative API for you to use. All you need to do is declare a function, do your thing, and return a value.

For instance, if you want to dynamically set a target, you’ll need to return a Node.

1
2
3
4
5
new Clipboard('.btn', {
target: function(trigger) {
return trigger.nextElementSibling;
}
});

If you want to dynamically set a text, you’ll return a String.

1
2
3
4
5
new Clipboard('.btn', {
text: function(trigger) {
return trigger.getAttribute('aria-label');
}
});

For use in Bootstrap Modals or with any other library that changes the focus you’ll want to set the focused element as the container value.

1
2
3
new Clipboard('.btn', {
container: document.getElementById('modal')
});

Also, if you are working with single page apps, you may want to manage the lifecycle of the DOM more precisely. Here’s how you clean up the events and objects that we create.

1
2
var clipboard = new Clipboard('.btn');
clipboard.destroy();

Browser Support

This library relies on both Selection and execCommand APIs. The first one is supported by all browsers while the second one is supported in the following browsers.

  • Chrome 42+
  • Edge 12+
  • Firefox 41+
  • IE 9+
  • Opera 29+
  • Safari 10+

The good news is that clipboard.js gracefully degrades if you need to support older browsers. All you have to do is show a tooltip saying Copied! when success event is called and Press Ctrl+C to copy when error event is called because the text is already selected.

You can also check if clipboard.js is supported or not by running Clipboard.isSupported(), that way you can hide copy/cut buttons from the UI.

Bonus

A browser extension that adds a “copy to clipboard” button to every code block on GitHub, MDN, Gist, StackOverflow, StackExchange, npm, and even Medium.

Install for Chrome and Firefox.

Build Status
npm version
npm

🚨重要!!

很抱歉,这个项目已不再维护了,可能很长一段时间都不会更新了。
如果真的需要,请使用之前请一定留意 Issues 里已知的问题

演示一下

自己试试

点我直接进入演示页面

说明

在客户端压缩好要上传的图片可以节省带宽更快的发送给后端,特别适合在移动设备上使用。

为什么需要

  1. 已踩过很多坑,经过几个版本迭代,以及很多很多网友的反馈帮助、机型测试

    • 图片扭曲、某些设备不自动旋转图片方向,没有jpeg压缩算法..
    • 不支持new Blob,formData构造的文件size为0..
    • 还有某些机型和浏览器(例如QQX5浏览器)莫名其妙的BUG..
  2. 按需加载(会根据对应设备自动异步载入JS文件,节省不必要带宽)

  3. 原生JS编写,不依赖例如jquery等第三方库,支持AMD or CMD规范。

尽管如此,在某些 Android 下依然有莫名其妙的问题,在您使用前,请一定大致浏览下 issues

如何获取

通过以下方式都可以下载:

  1. 执行npm i lrz(推荐)
  2. 执行bower install lrz

接着在页面中引入

1
<script src="./dist/lrz.bundle.js"></script>

如何使用

方式1:

如果您的图片来自用户拍摄或者上传的,您需要一个input file来获取图片。

1
<input id="file" type="file" accept="image/*" />

接着通过change事件可以得到用户选择的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
document.querySelector('#file').addEventListener('change', function () {
lrz(this.files[0])
.then(function (rst) {
// 处理成功会执行
console.log(rst);
})
.catch(function (err) {
// 处理失败会执行
})
.always(function () {
// 不管是成功失败,都会执行
});
});

方式2:

如果您的图片不是来自用户上传的,那么也可以直接传入图片路径。

1
2
3
4
5
6
7
8
9
10
lrz('./xxx/xx/x.png')
.then(function (rst) {
// 处理成功会执行
})
.catch(function (err){
// 处理失败会执行
})
.always(function () {
// 不管是成功失败,都会执行
});

后端处理

后端处理请查看WIKI。

API

具体参数说明请查看WIKI。

兼容性

IE10以上及大部分非IE浏览器(chrome、微信什么的)

FAQ

有疑问请直接在 issues 中提问

1
2
3
4
5
6
7
请一定记得附上以下内容:💡
请一定记得附上以下内容:🙈
请一定记得附上以下内容:💡

平台:微信..
设备:iPhone5 IOS7..
问题:问题描述呗..
  • Q:能否提供完整的一套UI?

  • A:暂时定位是作为纯粹的处理插件,或许会考虑开发一整套UI。

  • Q:有时拍摄完照片后,页面自动刷新或闪退了。

  • A:虽然已作了优化处理,但内存似乎还是爆掉了,常见于低配android手机,建议每次只处理一张图片。

  • Q: 怎么批量上传图片?

  • A: 您可以自己写个循环来传入用户多选的图片,但在移动端上请谨慎处理,原因同上。

  • Q: 直接传入图片路径的无法生成图片

  • A: 可能是跨域的问题,具体请看CORS_enabled_image

  • Q: 想要商用可以吗?

  • A: 没问题,但请留意issue里已知的问题。

开发

想要参与 or 自己定制 or 了解源码请点击这里,逻辑和说明

感谢

  • @dwandw
  • @yourlin
  • @wxt2005

以上在之前的版本帮忙参与维护的朋友,以及提出问题的朋友们,真的真的很感谢你们。:D

curl上传或者下载,有以下2个选项:

1
2
CURLOPT_NOPROGRESS => false,
CURLOPT_PROGRESSFUNCTION => 'callback',
  • CURLOPT_NOPROGRESS:是否关闭传输进度,默认是true。
  • CURLOPT_PROGRESSFUNCTION:回调函数,curl传输过程中,会每隔一段时间自动调用该函数。我测试过,间隔不到1秒,具体不知道。官方的注释是这样:设置一个回调函数,有五个参数,第一个是cURL的资源句柄,第二个是预计要下载的总字节(bytes)数。第三个是目前下载的字节数,第四个是预计传输中总上传字节数,第五个是目前上传的字节数。(注意回调函数的命名空间。如:CURLOPT_PROGRESSFUNCTION => ‘namespace_xxx\callback’)

设置完成后,需要定义回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function callback($resource, $downloadSize = 0, $downloaded = 0, $uploadSize = 0, $uploaded = 0)
{
// php5.5之前的参数是不同的,所以要考虑到兼容性
if (version_compare(PHP_VERSION, '5.5.0') > 0) {
$info = array(
'downloadSize' => $downloadSize,
'downloaded' => $downloaded,
'uploadSize' => $uploadSize,
'uploaded' => $uploaded,
);
} else {
$info = array(
'downloadSize' => 0,
'downloaded' => 0,
'uploadSize' => $downloaded,
'uploaded' => $uploadSize,
);
}

S('file_upload_' . session('user_auth.uid'), $info, 300); // 可以将结果存放到缓存(这里是ThinkPHP例子)
}

重要:
在curl发起请求时,如果开启了 session,会独占 session ,阻塞其他的请求。所以如果框架默认启用了 session ,在 curl 之前可以用session_write_close() 函数关闭 session 阻塞。
参考:http://www.cnblogs.com/skillCoding/archive/2012/04/09/2439296.html

最后:在进行传输时,可以每隔1秒通过ajax来获取缓存信息,从而显示传输进度。

补充:
传送大文件时,php会超时,注意设置 php-fpm.conf 中的 request_terminate_timeout 值,我设了1000(秒)。
还有个 max_children(进程数) ,进程不够用可改大。
在程序中,可以使用 set_time_limit() 临时增加 php 响应时间。
php.ini中还有 max_execution_time 设置,看攻略说是跟 set_time_limit 累加的,如果攻略是对的,那么这个不用管。

原文链接:https://blog.csdn.net/buer2202/article/details/75364939

我们可以在 <script> 片断中定义一个被JS调用的代码,但代码又不在页面上显示,这时,我们可以使用下面的方法:

1
2
3
4
5
6
7
8
9
<script id="commentTemplate" type="text/html">
<li>
<div class="photo">
<a href="#"><img src="[UserImg]" /></a>
</div>
<p><a href="#">[UserName]:</a><span class="time">[CreateDate]</span></p>
<div class="clear"></div>
</li>
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="comment_ul_2"></div>

<input type="button" id="addFun" value="click me" />

<script type="text/javascript">
var reg = new RegExp("\\[([^\\[\\]]*?)\\]", 'igm'); //i g m是指分别用于指定区分大小写的匹配、全局匹配和多行匹配。
$("#addFun").click(function () {
var html = document.getElementById("commentTemplate").innerHTML;
var source = html.replace(reg, function (node, key) { return { 'UserImg': '1', 'UserName': 'zhang', 'CreateDate': '2011-1-1'}[key]; });
$("#comment_ul_2").append(source);
});

var zzl = "name:[name]";
zzl = zzl.replace(reg, function (node, key) { return { 'name': '占占'}[key]; });
alert(zzl);
</script>

OK,这个意思是说,当你单击按钮时,可以把 commentTemplate 的内容追到 comment_ul_2 里,这很有意思吧,呵呵!

而其中有一个 replace ,也很有意思,向在替换时,可以接受一个 json 字符串,然后根据 json 的 key 来对比 js 模块里的 key ,进行赋值!

真的很有意思!

一款开源指纹识别工具。



开源工具使用说明:
-u 一个域名或IP,如果输入更多域名或IP使用‘,’分隔;
-r 读取本地域名或IP文件地址;
-t 输入一个线程数量,默认线程为50;
-p 设置一个请求端口,默认端口为80;
-s 设置一个请求协议,默认请求协议为http.(http、https);
-h 查看使用帮助;
-o 输出识别结果到本地文件;
-m 选择一个常规识别模式,默认使用模式为1;(1:快速识别一条指纹 2:获取命中率高的指纹 3:得到所有匹配的指纹)
–http-request 设置一个自定义请求URL;
–http-response 设置一个自定义关键字符。(支持正则表达式字符,自定义枚举指纹字符)
注:自定义识别模式参数为 –http-request /robots.txt –http-response discuz;不能和-m 常规识别模式一起使用。-u和-r参数不能同时使用。

  1. 使用-m参数选择常规识别模式,有3种可选模式。下面具体说明:
     -m 1:快速识别一条指纹,请求设置的url,获取相应识别方式的响应信息,进行数据库指纹遍历匹配,匹配到一条则跳出程序,显示识别结果。(速度较快)
     -m 2: 获取命中率高的指纹,例如请求url,进行指纹库全部遍历匹配,比如一个网址,命中了1个dedecms,2个 phpcms,1个discuz,那么显示的识别结果为phpcms程序。
     -m 3: 获取匹配识别所有的指纹信息,请求url获取响应信息,进行指纹库全部遍历匹配,最后获取指纹识别所有结果。(速度较慢、数据最全)
  2. 使用--http-request、--http-response参数自定义识别模式,下面具体说明:
     使用自定义识别模式应同时使用--http-request、--http-response两个参数,第一个参数设置为请求路径,第二个参数设置响应信息关键字符(支持正则表达式、不过注意大小写问题)
  
  例:java -jar Dayu.jar -r d:\\1.txt -t 100 --http-request / --http-response tomcat
      java -jar Dayu.jar -u www.discuz.net,www.dedecms.com -o d:\\result.txt
      java -jar Dayu.jar -u cn.wordpress.org -s https -p 443  -m 3
      

Dayu.jar程序说明:
Feature.json指纹文件放到D盘根目录(d:\Feature.json),如无D磁盘,请自行下载源码更改org.secbug.conf下Context.java文件中的currpath常量。

在此说明:
指纹识别离不开指纹库的强大。希望用户多多在我们平台 http://www.secbug.org:8080/ 提交指纹,我们一起进步。

  ☆☆ 本版本为第二版,第一版基于数据库sql文件,可保存指纹识别结果。如有需要,请联系QQ212125278所取。

https://github.com/Ms0x0/Dayu1

在 linux 下完整的用 wget 命令整站采集网站做镜像的命令是:

1
wget -m -e robots=off -U "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6" "http://www.example.com/"

wget命令参数注释:

-e robots=off #让 wget 耍流氓无视 robots.txt 协议

-U "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6" #伪造 agent 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class WS {
var $master; // 连接 server 的 client
var $sockets = array(); // 不同状态的 socket 管理
var $handshake = false; // 判断是否握手

function __construct($address, $port){
// 建立一个 socket 套接字
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
or die("socket_create() failed");
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1)
or die("socket_option() failed");
socket_bind($this->master, $address, $port)
or die("socket_bind() failed");
socket_listen($this->master, 2)
or die("socket_listen() failed");

$this->sockets[] = $this->master;

// debug
echo("Master socket : ".$this->master."\n");

while(true) {
//自动选择来消息的 socket 如果是握手 自动选择主机
$write = NULL;
$except = NULL;
socket_select($this->sockets, $write, $except, NULL);

foreach ($this->sockets as $socket) {
//连接主机的 client
if ($socket == $this->master){
$client = socket_accept($this->master);
if ($client < 0) {
// debug
echo "socket_accept() failed";
continue;
} else {
//connect($client);
array_push($this->sockets, $client);
echo "connect client\n";
}
} else {
$bytes = @socket_recv($socket,$buffer,2048,0);
print_r($buffer);
if($bytes == 0) return;
if (!$this->handshake) {
// 如果没有握手,先握手回应
$this->doHandShake($socket, $buffer);
echo "shakeHands\n";
} else {

// 如果已经握手,直接接受数据,并处理
$buffer = $this->decode($buffer);
//process($socket, $buffer);
echo "send file\n";
}
}
}
}
}

function dohandshake($socket, $req)
{
// 获取加密key
$acceptKey = $this->encry($req);
$upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $acceptKey . "\r\n" .
"\r\n";

echo "dohandshake ".$upgrade.chr(0);
// 写入socket
socket_write($socket,$upgrade.chr(0), strlen($upgrade.chr(0)));
// 标记握手已经成功,下次接受数据采用数据帧格式
$this->handshake = true;
}


function encry($req)
{
$key = $this->getKey($req);
$mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}

function getKey($req)
{
$key = null;
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) {
$key = $match[1];
}
return $key;
}

// 解析数据帧
function decode($buffer)
{
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;

if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}

// 返回帧信息处理
function frame($s)
{
$a = str_split($s, 125);
if (count($a) == 1) {
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o) {
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}

// 返回数据
function send($client, $msg)
{
$msg = $this->frame($msg);
socket_write($client, $msg, strlen($msg));
}
}

// 测试
$ws = new WS("127.0.0.1",2000);

纪念碑谷 2 上线 App Store 过去一天了,有不少朋友也已经迫不及待地下载并通关了。当然,其中也包括了 AppSo。

前天,AppSo(微信号 AppSo)简单地介绍了纪念碑谷 2,那么今天我们就为大家带来背后团队的采访和详细的评测吧。

它仍是我们喜欢的纪念碑谷

纪念碑谷原来的设计师 Ken Wong 已经离开了团队,回到了澳洲,所以这次的设计团队也是一个完全不一样的团队,有不少人担心它的游戏内核会不会发生变化。

纪念碑谷 2 延续了纪念碑谷 1 的玩法,你需要利用视错觉让主角走出迷宫。设计依旧以艺术家埃舍尔的创作为基础,利用了「潘洛斯三角」、「凹凸错觉」「二维三维的扭转变换」……画风延续之余,但剧情发生了较大的变化。

相信大家都记得第一代的纪念碑谷有着相对阴郁的剧情,它的主题是「宽恕」。而纪念碑谷 2 的基调就变得轻快起来——它探讨的是一个「亲子关系」的故事。

继前作的「艾达公主」之后,此次带领我们踏上旅程的是「罗尔」和她的孩子。在旅途中,你将帮助主角罗尔教导她的小孩。

极富想象力的场景,和无论色调、元素单一或复杂都至臻完美的画面,带来视听享受的同时,令我们倍感熟悉。

不同的是,这趟旅途少了孤单,多了陪伴。目的也不再是寻觅,而是互相扶持着携手并进。

孩子奔跑、跌倒,无力遥望坠落的母亲时,我们不免揪心。

当两人终于跨过险阻冲向终点时,我们深深感动。

剧情已经足够丰富,「纪念碑谷 2」却未就此停下。

关卡不断推进,冷静质朴的文字揭示了更为残忍的事实——孩子终将长大成人,伴随他们度过人生旅程的父母也将回归孤独,再度独自踏上旅程。

顺逆相随,让她羽翼自丰。
面对生命进程,我们不乏勇气,而是放手的决心。
你,终将继续完成属于自己的旅程。

这种安排无疑会令玩家怅然若失,却也成就了纪念碑谷 2 的超高人气。

然而,为什么会选择这样的故事?

见纪念碑谷团队的时候,是在一个布置温馨的小房间,游戏总监 Dan Gray 脸上带着轻松的笑容——纪念碑谷 2 如期成功上线,在 WWDC 上进行了一次短暂的亮相。

在采访前,我已经到达了第五章,而游戏总监 Dan Gray 看到则说:

Aha, you go far.

纪念碑谷 2 一共有 14 个关卡,耗费 15 个月完成,比起纪念碑谷少了 4 个关卡。而不少通关的朋友也表示,相对于纪念碑谷,纪念碑谷 2 的难度有所降低,但依旧精美。

纪念碑谷艾达的故事已经完结了,它是一个完整的故事。

纪念碑谷游戏制作人 Adrienne Law 告诉 AppSo(微信号 AppSo),团队不想为了续作而去专门设计情节去承接实际上已经完整的故事,而且有大约 1/3 的新人加入团队,对故事充满了激情,有很多新想法,需要一个全新剧本去承载。

而 Dan Gray 也补充,希望通过开启一个新故事,让没有玩过纪念碑谷 1 的朋友,直接上手纪念碑谷 2 的时候,不会觉得错过了剧情。

但其实纪念碑谷 1 和 2 之间有很微妙的连通,只是藏得比较深,需要玩家仔细留意。

至于为什么选择了「亲子关系」这个主题,他们是这样看的:

很少有游戏触及母子关系的主题,纪念碑谷的玩家也会长大,我们自己也为人子女,在长大的过程中对母亲的想法会有所改变,所以会尝试探索这个话题。随着孩子不断成长,母亲和孩子的关系会不断地变化。

而我们在评测时也发现,这样的「探索」也反映在「双人关卡」的设计上。在部分关卡里,女儿会以母亲为「榜样」——我们控制罗尔移动,女儿也会进行相应的走位。需要两个人互相配合才能通关。

纪念碑谷的成功能复制吗?

像纪念碑谷这样现象级的独立游戏屈指可数,而能真正做到「叫好又叫座」的更是凤毛麟角。纪念碑谷的盈利模式很简单,就是单纯地买游戏,和国内靠卖装备卖金币真正赚钱的「氪金手游」不同。

这种单纯卖游戏的商业模式,同样适用于别的独立应用/游戏吗?

Dan Gray 给出了这样的回答:

看目标是什么。有很多小公司其实能一直以这样的方式做下去。如果保持公司现有的规模,则能维持经营。不过,一旦公司做大了,就不得不选择其他商业模式保持运营。

当然,纪念碑谷团队也会希望更多中国玩家接触这款游戏,同时让纪念碑谷这个 IP 有更多可能性的发展,他们选择了腾讯作为代理。

腾讯游戏是国内最大的移动游戏平台,而且有庞大的社区和用户群体。因此无论是用户量还是游戏 IP 的包装,我们可以期待纪念碑谷能有更多新的可能:

我们是个付费游戏,让更多的人知道我们,需要腾讯这样的公司来代理,扩大影响力。

无论选择什么代理公司,也不会影响它作为独立游戏的本质。初次开启时你可选择微信或 QQ 登录,这能让你分享图片到朋友圈等平台。当然你也可以选择游客模式,这不会对后续的关卡造成任何影响。

除此之外,纪念碑谷也会出自己的周边产品,让大家更立体地体验游戏世界。

目前,纪念碑谷 2 已上线各区 App Store,售价 30 元,而 Android 平台暂时未有上线计划。

因为大部分独立开发者都是开发付费游戏,而 iTunes 上付费 app 的推广效果最好。为了保持营收,所以大部分独立游戏都会先上 iOS 版本,或者干脆只有 iOS 版本。

不同人的选择不同,而 iOS 用户更倾向为高质量的游戏付费。除此之外,苹果也会提供更好的支持(纪念碑谷 2 已经横扫 App Store 推广位了),所以也是纪念碑谷团队选择 iOS 平台原因。

AppSo 当然知道,有很多愿意为优质 app 付费的 Android 用户,也有很多不愿意花钱的 iOS 用户,但平台给创作者的回馈不同,创作者自然会予以相应的选择。

这也是为什么 AppSo 一直倡导正版的原因:

你花的每一分钱,都在为你想要的世界买单。