`

Sprite Kit编程指南(1)-深入Sprite Kit

    博客分类:
  • ios
阅读更多
Sprite Kit编程指南(1)-深入Sprite Kit
深入Sprite Kit

学习Sprite Kit最好的方法是在实践中观察它。此示例创建一对场景和各自的动画内容。通过这个例子,你将学习使用Sprite Kit内容的一些基础技术,包括:

·      场景在一个基于Sprite Kit的游戏中的角色。

·      如何组织节点树来绘制内容。

·      使用动作让场景内容动起来。

·      如何添加交互到场景。

·      场景之间的过渡。

·      在一个场景里模拟物理。

一旦你完成这个项目,你可以用它来试验其他Sprite Kit概念。你可以在这个例子的结尾找到一些建议。

你应该已经熟悉创建iOS应用程序之前通过这个项目工作。欲了解更多信息,请参阅今天开始开发iOS应用程序的。大多数Sprite Kit在这个例子中的代码是相同的OS X。


让我们开始吧

本次练习需要Xcode 5.0。使用的单一视图的应用程序模板创建一个新的iOS应用程序的Xcode项目。

在创建项目时,请使用以下值:

·      产品名称:SpriteWalkthrough

·      ClassPrefix:Sprite

·      设备:iPad

添加Sprite Kit框架到项目中。


创建你的第一个场景

Sprite Kit内容被放置在一个窗口中,就像其他可视化内容那样。Sprite Kit内容由SKView类渲染呈现。SKView对象渲染的内容称为一个场景,它是一个SKScene对象。场景参与响应链,还有其他使它们适合于游戏的功能。

因为Sprite Kit内容由视图对象渲染,你可以在视图层次组合这个视图与其他视图。例如,你可以使用标准的按钮控件,并把它们放在你的Sprite Kit视图上面。或者,你可以添加交互到精灵来实现自己的按钮,选择权在你。在这个例子中,稍候你会看到如何实现场景交互。


配置视图控制器来使用Sprite Kit



1.    打开项目的storyboard。它有一个单一的视图控制器(SpriteViewController)。选择视图控制器的view对象并把它的类改成SKView。

2.    在视图控制器的实现文件添加一个导入行。

#import <SpriteKit/SpriteKit.h>
3.    实现视图控制器的viewDidLoad方法来配置视图。



- (void)viewDidLoad
{
    [super viewDidLoad];
    SKView * spriteView =(SKView *)self.view;
    spriteView.showsDrawCount = YES;
    spriteView.showsNodeCount = YES;
    spriteView.showsFPS = YES;
}


4.    代码开启了描述场景如何渲染视图的诊断信息。最重要的一块信息是帧率( spriteView.showsFPS),你希望你的游戏尽可能在一个恒定的帧率下运行。其他行展示了在视图中显示了多少个节点,以及使用多少绘画传递来渲染内容(越少越好)的详情。

接下来,添加第一个场景。


创建Hello场景



1.    创建一个名为HelloScene新类并让它作为SKScene类的子类。

2.    在你的视图控制器导入场景的头文件。



#import “HelloScene.h”


3.    修改视图控制器来创建场景,并在视图中呈现场景。



- (void)viewWillAppear:(BOOL)animated
{
    HelloScene *hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)];
    SKView *spriteView =(SKView *)self.view;
    [spriteView presentScene:hello];
}


现在,构建并运行项目。该应用程序应该启动并显示一个只有诊断信息的空白屏幕。


将内容添加到场景

当设计一个基于Sprite Kit的游戏,你要为你的游戏界面各主要大块(chuck)设计不同的场景类。例如,你可以为主菜单创建一个场景而为游戏设置创建另一个单独的场景。在这里,你会遵循类 似的设计。这第一个场景显示了传统的“Hello World”文本。

大多数情况下,你可以配置一个场景在它被视图首次呈现时的内容。这跟视图控制器只在视图属性被引用时加载他们的视图的方式是类似的。在这个例子中,代码在didMoveToView:方法内部,每当场景在视图中显示时该方法会被调用。


在场景中显示Hello文本



1.    添加一个新的属性到场景的实现文件中来跟踪场景是否已创建其内容。



@interface HelloScene()
@property BOOL contentCreated;
@end

该属性跟踪并不需要向客户端公开的状态,所以,在实现文件中它一个私有接口声明里实现。



2.    实现场景的didMoveToView:方法。



- (self)didMoveToView:(SKView *)view
{
    if(!self.contentCreated)
    {
        [self createSceneContents];
        self.contentCreated = YES;
    }
}
每当视图呈现场景时, didMoveToView:方法都会被调用。但是,在这种情况下,场景的内容应只在场景第一次呈现时进行配置。因此,这段代码使用先前定义的属性( contentCreated)来跟踪场景的内容是否已经被初始化。



3.    实现场景的createSceneContents方法。



- (void)createSceneContents
{
    self.backgroundColor = [SKColor blueColor];
    self.scaleMode = SKSceneScaleModeAspectFit;
    [self AddChild:[self newHelloNode];
}


场景在绘制它的子元素之前用背景色绘制视图的区域。注意使用SKColor类创建color对象。事实上,SKColor不是一个类,它是一个宏,在iOS上映射为UIColor而在OS X上它映射为NSColor。它的存在是为了使创建跨平台的代码更容易。

场景的缩放(scale)模式决定如何进行缩放以适应视图。在这个例子中,代码缩放视图,以便你可以看到场景的所有内容,如果需要使用宽屏(letterboxing)。

4.    实现场景的newHelloNode方法。



- (SKLabelNode *)newHelloNode
{
    SKLabelNode * helloNode = [SKLabelNode labelNodeWithFontNamed:@“Chalkduster”];
    @helloNode.text =“Hello, World!”
    helloNode.fontSize = 42;
helloNode.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
    return helloNode;
}
你永远不用编写显式执行绘图命令的代码,而如果你使用OpenGL ES或Quartz 2D你就需要。在Sprite Kit中,你通过创建节点对象并把它们添加到场景中来添加内容。所有绘制必须由Sprite Kit中提供的类来执行。你可以自定义这些类的行为来产生许多不同的图形效果。然而,通过控制所有的绘图,Sprite Kit可以对如何进行绘图应用许多优化。



现在构建并运行该项目。你现在应该看到一个蓝色屏幕上面有“Hello, World!”。现在,你已经学会了绘制Sprite Kit内容的所有基础知识。


使用动作让场景动起来

静态文本很友好,但如果文字可以动起来,它会更有趣。大多数的时候,你通过执行动作(action)移动场景周围的东西。Sprite Kit中的大多数动作对一个节点应用变化。创建action对象来描述你想要的改变,然后告诉一个节点来运行它。然后,当渲染场景时,动作被执行,在几个帧上发生变化直到它完成。

当用户触摸场景内容,文字动起来然后淡出。


让文本动起来



1.    添加以下代码到newHelloNode方法:



helloName.name = @“helloNode”;


所有节点都有一个名称属性,你可以设置它来描述节点。当你想能够在稍后找到它,或当你想构建基于节点名称的行为时,你应该命名一个节点。稍后,你可以搜索树中与名称相匹配的节点。

在这个例子中,你给标签的一个名称以便稍后可以找到它。在实际的游戏中,你可能会得给呈现相同类型的内容的任何节点以相同的名称。例如,如果你的游戏把每个怪物呈现为一个节点,你可能会命名节点为monster。

2.    重载场景类的touchesBegan:withEvent方法。当场景接收到触摸事件,它查找名为helloNode的节点,并告诉它要运行一个简短的动画。

所有节点对象都是iOS上UIResponder 或OS X上NSResponder的 的子类。这意味着你可以创建Sprite Kit节点类的子类来添加交互到场景中的任何一个节点。



- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    SKNode *helloNode = [self childNodeWithName:@“helloNode”];
    If(helloNode != nil)
    {
        helloNode.name = nil;
        SKAction *moveUp = [SKAction moveByX:0 y:100.0 duration:0.5];
        SKAction *zoom = [SKAction scaleTo:2.0 duration:0.25];
        SKAction *pause = [SKAction waitForDuration:0.5];
        SKAction *fadeAway = SKAction fadeWithDuration:0.25];
        SKAction *remove = [SKAction removeFromParent];
        SKAction * moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove];
        [helloNode runAction:moveSequence];
    }
}

为了防止节点响应重复按压,代码会清除节点的名称。然后,它构建动作对象来执行各种操作。最后,它组合这些动作创建一个 动作序列;序列运行时,按顺序执行每个动作。最后,它告诉标签节点执行序列动作。



运行的应用程序。你应该看到像之前那样的文字。在屏幕的底部,节点计数应该是1。现在,点击视图内部。你应该看到文字动画并淡出。在它淡出后,节点计数应该变为0,因为节点已从父节点中删除。


场景之间的转换

Sprite Kit让场景之间的过渡变得很容易。场景之间的过渡时,你可以坚持保留它们,或清除它们。在这个例子中,你将创建第二个场景类,来学习一些其他的游戏行为。“Hello, World!”文字从屏幕上消失时,代码创建一个新的场景并过渡到它。Hello场景过渡在后会被丢弃。


创建飞船场景



1.    创建一个名为SpaceshipScene的新类并让它成为SKScene类的子类。

2.    实现代码来初始化飞船场景的内容。此代码类似于你为HelloScene类实现的代码。



@interface SpaceshipScene()
@property BOOL contentCreated;
@end

@implementation SpaceshipScene
- (void)didMoveToView:(SKView *)view
{
    If(!self.contentCreated)
    {
        [self createSceneContents];
        self.contentCreated = YES;
    }
}

- (void)createSceneContents
{
    self.backgroundColor = [SKColor blackColor];
    self.scaleMode = SKSceneScaleModeAspectFit;
}
3.    在 HelloScene.m文件中导入 SpaceshipScene.h头。





#import "SpaceshipScene.h"

4.    在 touchesBegan:withEvent方法中,更改 runAction: 的调用为新的调用 runAction:completion:。实现完成处理来创建并呈现一个新的场景。





[helloNode runAction:moveSequence completion:^ {
    SKScene * spaceshipScene = [[SpaceshipScene alloc] initWithSize:self.size];
    SKTransition *doors= [SKTransition doorsOpenVerticalWithDuration:0.5];
    [self.view presentScene:spaceshipScene transition:doors];
}];
构建并运行该项目。当你触摸场景内部时,文字淡出,然后在视图过渡到新的场景。你应该看到一个黑色的屏幕。




使用节点构建复杂的内容

新的场景还没有任何内容,所以你准备要添加一个飞船到场景。要构建这个太空飞船,你需要使用多个SKSpriteNode对象来创造了飞船和它表面的灯光。每个精灵节点都将执行动作。

精灵节点是在一个Sprite Kit应用程序中最常见用于创建内容的类。他们可以绘制无纹理或纹理的矩形。在这个例子中,你要使用无纹理对象。稍后,这些占位符(placeholder)可以很容易地用纹理精灵进行替换,而不改变它们的行为。在实际的游戏中,你可能需要几十个或上百个节点来创建你的游戏的可视化内容。但是,从本质上说,那些精灵将使用与这个简单的例子相同 的技术。

虽然你可以直接添加所有三个精灵到场景,但这并不是Sprite Kit的方式。闪烁的灯光是飞船的一部分!如果飞船移动,灯光应该和它一起移动。解决的办法是使飞船节点成为它们的父节点,同样地场景将是飞船的父节点。光的坐标将要相对于父节点的位置来指定,而父节点是在子精灵图像的中心。


添加飞船



1.    在SpaceshipScene.m中,添加代码到createSceneContents方法来创建飞船。



SKSpriteNode *spaceship = [self newSpaceship];
spaceship.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame)-150);
[self addChild:spaceship];

2.    实现 newSpaceship 的方法。





- (SKSpriteNode *)newSpaceship
{
SKSpriteNode *hull= [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32);

    SKAction *hover= [SKAction sequence:@[
                          [SKAction waitForDuration:1.0]
                          [SKAction moveByX:100 y:50.0 duration:1.0]
                          [SKAction waitForDuration:1.0]
                          [SKAction moveByX:-100.0 y:-50 duration:1.0]];
    [hull runAction:[SKAction repeatActionForever:hover];

    return hull;}

此方法创建飞船的船体,并添加了一个简短的动画。需要注意的是引入了一种新的动作。一个重复的动作不断地重复的传递给它的动作。在这种情况下,序列一直重复。



现在构建并运行应用程序来看当前的行为,你应该看到一个矩形。

在建立复杂的有孩子的节点时,把用来在构造方法后面或者甚至是在子类中创建节点的代码分离出来,是一个很好的主意。这使得它更容易改变精灵的组成和行为,而无需改变使用精灵的客户端(client)。

3.    添加代码到newSpaceship方法来添加灯光。



SKSpriteNode *light1= [self newLight];
light1.position = CGPointMake(-28.0,6.0);
[hull addChild:light1];

SKSpriteNode *light2= [self newLight];
Light2.position = CGPointMake(28.0,6.0);
[hull addChild:light2];
4.    实现newLight方法。





- (SKSpriteNode *)newLight
{
SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)];

    SKAction *blink= [SKAction sequence:@ [
                          [SKAction fadeOutWithDuration:0.25]
                          [SKAction fadeInWithDuration:0.25]];
    SKAction * blinkForever = [SKAction repeatActionForever:blink];
    [light runAction:blinkForever];

    return light;
}
当你运行应用程序时,你应该看到一对灯在飞船上。当飞船移动,灯光和它一起移动。这三个节点全都是连续动画。你可以添加额外的动作,让灯光在船的周围移动,它们总是相对船体移动。




创建能交互的节点

在实际的游戏中,你通常需要节点之间能交互。把行为添加给精灵的方法有很多,所以这个例子仅展示其中之一。你将添加新节点到场景,使用物理子系统模拟它们的运动并实现碰撞效果。

Sprite Kit提供了一个完整的物理模拟,你可以使用它添加自动行为到节点。也就是说,物理在使其移动的节点上自动模拟,而不是在节点上执行动作。当它与物理系统一部分的其他节点交互时,碰撞自动计算并执行。


添加物理模拟到飞船场景



1.    更改newSpaceship方法来添加一个物理体到飞船。



hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];

构建并运行应用程序。等一下!飞船垂直坠落到屏幕下方。这是因为重力施加到飞船的物理体。即使移动动作仍在运行,物理效果也被应用到飞船上。



2.    更改的newSpaceship方法来防止飞船受物理交互影响。



hull.physicsBody.dynamic = NO;
当你现在运行它时,应用程序像之前那样运行。飞船不再受重力影响。稍后,这也意味着飞船的速度将不会受到碰撞的影响,。



3.    添加代码到createSceneContents方法来生成大量岩石。



SKAction * makeRocks = [SKAction sequence:@ [
    [SKAction performSelector:@selector(addRock) onTarget:self]
    [SKAction waitForDuration:0.10 withRange:0.15]
    ]];
[self runAction:[SKAction repeatActionForever:makeRocks];
场景也是一个节点,因此它也可以运行动作。在这种情况下,自定义操作调用场景上的方法来创建岩石。序列创建一个岩石,然后等待一段随机时间。重复这个动作,场景不断产生大量新的岩石。



4.    实现addRock方法。



static inline:CGFloat skRandf() {
    return rand()/(CGFloat)RAND_MAX;
}

static inline CGFloat skRand(CGFloat low, CGFloat high) {
    return skRandf()*(high - low) + low;
}

- (void)addRock
{
SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)];
    rock.position = CGPointMake(skRand(0, self.size.width),self.size.height-50);
    rock.name = @“rock”;
    rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
    rock.physicsBody.usesPreciseCollisionDetection = YES;
    [self addChild:rock];
}
构建并运行该项目。岩石现在应该从场景上方落下来。当一块石头击中了船,岩石从船上反弹。没有添加动作来移动岩石。岩石下落并与船碰撞完全是由于物理子系统的作用。



岩石都很小且移动速度非常快,所以代码指定精确的碰撞,以确保所有的碰撞都检测到。

如果你让应用程序运行了一段时间,帧率会开始下降,即使节点计数仍然很低。这是因为节点的代码仅显示出场景中可见的节点。然而,当岩石落下到场景的底部时,它们继续存在于场景中,这意味着物理还在对它们模拟。最终,有如此多的节点正在处理以致Sprite Kit减慢了。

5.    实现场景中的didSimulatePhysics方法来当岩石移动到屏幕之外时移除它们。



- (void)didSimulatePhysics
{
    [self enumerateChildNodesWithName:@“rock” usingBlock:^(SKNode *node, BOOL *stop){
        if (node.position.y <0)
            [node removeFromParent];
    }];
}
每次场景处理一帧,都运行动作和模拟物理。你的游戏可以挂接到这个过程中来执行其他自定义代码。在每一帧,场景将处理物理,然后移除移出屏幕底部的所有岩石。当你运行应用程序时,帧率保持不变。



在场景中,预处理及后处理与动作和物理结合的地方,就是你建立你的游戏的行为的地主。

这就是你第一次体验Sprite Kit!其它一切都是你在这里看到的基本技术的细化。


试试这个!

这里有一些东西,你可以尝试:

·      做一个OS X版本的这个例子。你在视图控制器写的代码,在OS X上通常是在一个应用程序委托中实现。响应代码需要改变来使用鼠标事件而不是触摸事件。但是,代码的其余部分应是相同的。

·      使用纹理精灵呈现船和岩石。(提示:“使用精灵”)

·      尝试在触摸事件的响应中移动飞船。(提示:“添加动作节点”和“构建场景”)。

·      添加额外的图形效果到场景(提示:“使用其他节点类型”)

·      岩石与船舶碰撞时添加其他行为。例如,使岩石发生爆炸。(提示:“模拟物理”)
分享到:
评论

相关推荐

    Sprite Kit编程指南(中文版)

    Sprite Kit编程指南(中文版)是苹果官方英文文档的中文翻译版本,旨在帮助不懂英文的开发者也能理解和运用Sprite Kit。该指南详细介绍了Sprite Kit的各种特性、类、方法以及如何在Xcode中使用Sprite Kit进行游戏...

    Sprite Kit编程指南

    Sprite Kit编程指南详细介绍了如何使用这个框架来创建游戏。 首先,Sprite Kit提供了图形渲染和动画的基础,使得开发者可以轻松地让纹理图像或精灵动起来。它采用传统的渲染循环,允许开发者在渲染前处理每一帧的...

    sprite kit 编程指南

    ### Sprite Kit编程指南:iOS游戏开发的利器 Sprite Kit是苹果公司为iOS、iPadOS以及macOS提供的2D游戏开发框架。它集成了强大的物理引擎、粒子系统、纹理图集管理以及动画支持等功能,旨在帮助开发者高效地创建高...

    spritekit-swift-physics-bouncing-balls-源码.rar

    "spritekit-swift-physics-bouncing-balls-源码"是一个示例项目,展示了如何在SpriteKit中利用Swift实现物理碰撞的弹跳球效果。 在这个项目中,我们可以学习到以下几个关键知识点: 1. **SpriteKit基本概念**: -...

    spine-spritekit, 针对 iOS 7 SpriteKit的非官方书脊 2D 运行时.zip

    spine-spritekit, 针对 iOS 7 SpriteKit的非官方书脊 2D 运行时 脊椎 spritekitUnofficial ( http://esotericsoftware.com )的非官方 iOS 7运行时运行时官方运行时在这里:http://esotericsoftware.com/spine-runti

    IOS 2D游戏开发框架 Spritekit案例

    - 官方文档:Apple提供了详细的SpriteKit编程指南和参考文档。 - 开源项目:GitHub上有许多开源的SpriteKit游戏,可以作为学习和参考的对象。 - 在线课程:多个在线教育平台提供专门的SpriteKit教程,适合初学者...

    使用SpriteKit写的时钟型技能cd动画

    下面我们将深入探讨如何使用SpriteKit来创建此类动画。 1. **SpriteKit基础** - SpriteKit提供了一个基于节点(Node)的图形系统,这些节点可以是图像、形状或动画等,它们可以通过父节点和子节点的关系进行组织。...

    ios7 spritekit 游戏demo

    在编程逻辑层面,SpriteKit提供了`SKShader`,允许开发者实现自定义的图形效果,如光照、粒子系统等。此外,`SKNode`的`runAction:`方法可以用来控制游戏逻辑,比如角色移动、攻击动作的执行。`SKAction`不仅限于...

    LevelHelper2-SpriteKit:LevelHelper2-SpriteKit

    此外,LevelHelper2-SpriteKit的文件列表"LevelHelper2-SpriteKit-master"可能包含以下内容: - 源代码文件:C++源码,实现了与LevelHelper编辑器的交互和在游戏中加载关卡的功能。 - 示例项目:包含了一个使用...

    SpriteKit物理系统Demo

    让我们深入探讨SpriteKit的物理系统以及它如何工作。 在SpriteKit中,物理世界是由`SKPhysicsWorld`对象管理的,它可以模拟真实世界的物理现象,如重力、碰撞检测和动力学。以下是一些关键知识点: 1. **物理Body*...

    sprite kit demo

    下面我们将深入探讨Sprite Kit的一些关键概念和技术。 1. **精灵(Sprites)**:在Sprite Kit中,精灵是2D游戏的基本元素,可以视为一个图像或一组图像。你可以将它们视为屏幕上的动态对象,如角色、道具或其他视觉...

    开始使用 SpriteKit 的演示。将著名的 Cocos2DSimpleGame 移植到 SpriteKit

    _ Objective-C_代码_下载 开始使用 SpriteKit 的演示。将著名的 Cocos2DSimpleGame 移植到 ...See my blog for more: SpriteKit快速入门和新时代iOS游戏开发指南 更多详情、使用方法,请下载后阅读README.md文件

    ios打飞机 ios 飞机 spritekit 简单的SpriteKit打飞机

    简单的SpriteKit打飞机 利用SpriteKit写的一个简单的打飞机场景,可以发射子弹、移动飞机以及碰撞检测。 利用SpriteKit写的一个简单的打飞机场景,可以发射子弹、移动飞机以及碰撞检测。

    iOS游戏开发之使用 Spritekit 框架和 Swift 的 iOS 2D 太空射击游戏源码

    本教程将深入探讨如何利用SpriteKit和Swift来创建一个2D太空射击游戏。 首先,SpriteKit提供了一个完整的图形引擎,包括精灵(Sprites)、物理引擎、动画和粒子系统等组件,这些都是构建游戏的基础。Swift作为Apple...

    sprite kit"割绳子"游戏详解

    对于初学者而言,需要先了解SpriteKit的基本元素如场景(Scene)、节点(Node)、精灵(Sprite)等。Swift语言是苹果官方推荐的iOS开发语言,因其简洁的语法和强大的功能受到开发者青睐,本教程使用的是Swift 3版本...

    SpriteKit 划线弹小球游戏。

    通过阅读和理解这些文件,你可以深入学习如何利用SpriteKit构建一个完整的2D游戏。 最后,为了提高游戏体验,我们还可以考虑以下优化: 1. 添加音效和背景音乐,增加沉浸感。 2. 设计不同级别的难度,增加游戏挑战...

    swift-基于SpriteKit的按钮等控件

    在Swift编程语言中,SpriteKit是一个强大的2D游戏开发框架,它允许开发者创建动画和交互式游戏场景。本文将深入探讨如何使用SpriteKit构建基于SKControl和SKButton的控件,这些控件对于构建游戏界面或任何需要用户...

    Sprite Kit Programming Guide

    《Sprite Kit 编程指南》是一本专门针对Sprite Kit框架的编程指导书籍。Sprite Kit是苹果公司为其操作系统开发的应用程序框架之一,主要用于2D游戏开发和动画制作。本指南首先介绍了Sprite Kit的基本概念和用途,...

    swift-使用SpriteKit实现iOS版本的2048游戏

    在Swift编程环境中, SpriteKit 是一个强大的2D游戏开发框架,专为构建iOS、macOS以及其他Apple平台上的游戏而设计。本教程将详细介绍如何利用SpriteKit来实现一款经典的2048游戏。2048是一款简单却令人上瘾的数字...

Global site tag (gtag.js) - Google Analytics