54、基于Sprite Kit的游戏开发全流程指南

基于Sprite Kit的游戏开发全流程指南

1. 游戏初步体验与敌人创建

运行游戏应用程序,你会看到飞船在来回移动时会愉快地摆动,看起来就像在行走一样。不过,这个游戏需要一些敌人供玩家射击。

首先,使用Xcode创建一个新的Cocoa Touch类,名为 EnemyNode ,并以 SKNode 作为父类。目前,我们不会给敌人类添加任何实际行为,但会赋予它外观。我们将使用与创建玩家相同的技术,用文本构建敌人的身体。以下是 EnemyNode.m 中的代码:

- (instancetype)init {
    if (self = [super init]) {
        self.name = [NSString stringWithFormat:@"Enemy %p", self];
        [self initNodeGraph];
    }
    return self;
}

- (void)initNodeGraph {
    SKLabelNode *topRow = [SKLabelNode
                           labelNodeWithFontNamed:@"Courier-Bold"];
    topRow.fontColor = [SKColor brownColor];
    topRow.fontSize = 20;
    topRow.text = @"x x";
    topRow.position = CGPointMake(0, 15);
    [self addChild:topRow];

    SKLabelNode *middleRow = [SKLabelNode
                              labelNodeWithFontNamed:@"Courier-Bold"];
    middleRow.fontColor = [SKColor brownColor];
    middleRow.fontSize = 20;
    middleRow.text = @"x";
    [self addChild:middleRow];

    SKLabelNode *bottomRow = [SKLabelNode
                              labelNodeWithFontNamed:@"Courier-Bold"];
    bottomRow.fontColor = [SKColor brownColor];
    bottomRow.fontSize = 20;
    bottomRow.text = @"x x";
    bottomRow.position = CGPointMake(0, -15);
    [self addChild:bottomRow];
}

这里只是通过改变每个文本“行”的y值来添加多行文本。

2. 将敌人添加到场景中

接下来,我们要让敌人出现在场景中,需要对 GameScene.m 进行一些修改:
1. 在文件顶部添加以下代码:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"

@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;

@end

这里导入了新敌人类的头文件,并添加了一个新属性来保存所有将添加到关卡中的敌人。使用 SKNode 来保存敌人是因为它可以容纳任意数量的子节点,便于管理。
2. 创建 spawnEnemies 方法:

- (void)spawnEnemies {
    NSUInteger count = log(self.levelNumber) + self.levelNumber;
    for (NSUInteger i = 0; i < count; i++) {
        EnemyNode *enemy = [EnemyNode node];
        CGSize size = self.frame.size;
        CGFloat x = arc4random_uniform(size.width * 0.8)
                    + (size.width * 0.1);
        CGFloat y = arc4random_uniform(size.height * 0.5)
                    + (size.height * 0.5);
        enemy.position = CGPointMake(x, y);
        [self.enemies addChild:enemy];
    }
}
  1. initWithSize:levelNumber: 方法的末尾添加以下代码:
[self addChild:_playerNode];
_enemies = [SKNode node];
[self addChild:_enemies];
[self spawnEnemies];

运行应用程序,你会看到可怕的敌人随机出现在屏幕的上半部分。

3. 开始射击功能的实现

现在是时候让玩家能够攻击敌人了。我们希望玩家可以点击屏幕上半部分的任意位置向敌人射击子弹。这里将使用Sprite Kit中包含的物理引擎来移动玩家的子弹,并检测子弹与敌人的碰撞。

3.1 物理引擎的概念

物理引擎是一个软件组件,它跟踪一个世界中的多个物理对象(通常称为物体)以及作用在它们上的力,确保一切以逼真的方式移动。它可以考虑重力、处理物体之间的碰撞,甚至模拟摩擦和弹性等物理特性。需要注意的是,物理引擎通常与图形引擎是分开的。

3.2 定义物理类别

Sprite Kit物理引擎允许我们将对象分配到几个不同的物理类别中。在这个游戏中,我们将创建三个类别:敌人、玩家和玩家导弹。以下是创建这些类别的代码:

#ifndef TextShooter_PhysicsCategories_h
#define TextShooter_PhysicsCategories_h

typedef NS_OPTIONS(uint32_t, PhysicsCategory) {
    PlayerCategory        =  1 << 1,
    EnemyCategory         =  1 << 2,
    PlayerMissileCategory =  1 << 3
};

#endif

这些类别使用位掩码,每个类别必须是2的幂,这样可以简化物理引擎的API。

3.3 创建BulletNode类

创建一个新的Cocoa Touch类 BulletNode ,以 SKNode 为父类。在头文件中声明两个公共方法:

#import <SpriteKit/SpriteKit.h>

@interface BulletNode : SKNode

+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination;
- (void)applyRecurringForce;

@end

BulletNode.m 中实现这些方法:

#import "BulletNode.h"
#import "PhysicsCategories.h"
#import "Geometry.h"

@interface BulletNode ()

@property (assign, nonatomic) CGVector thrust;

@end

@implementation BulletNode

- (instancetype)init {
    if (self = [super init]) {
        SKLabelNode *dot = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        dot.fontColor = [SKColor blackColor];
        dot.fontSize = 40;
        dot.text = @".";
        [self addChild:dot];

        SKPhysicsBody *body = [SKPhysicsBody bodyWithCircleOfRadius:1];
        body.dynamic = YES;
        body.categoryBitMask = PlayerMissileCategory;
        body.contactTestBitMask = EnemyCategory;
        body.collisionBitMask = EnemyCategory;
        body.mass = 0.01;

        self.physicsBody = body;
        self.name = [NSString stringWithFormat:@"Bullet %p", self];
    }
    return self;
}

+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination {
    BulletNode *bullet = [[self alloc] init];

    bullet.position = start;

    CGVector movement = VectorBetweenPoints(start, destination);
    CGFloat magnitude = VectorLength(movement);
    if (magnitude == 0.0f) return nil;

    CGVector scaledMovement = VectorMultiply(movement, 1 / magnitude);

    CGFloat thrustMagnitude = 100.0;
    bullet.thrust = VectorMultiply(scaledMovement, thrustMagnitude);

    return bullet;
}

- (void)applyRecurringForce {
    [self.physicsBody applyForce:self.thrust];
}
3.4 将子弹添加到场景中

GameScene.m 中添加子弹到场景:
1. 导入新类的头文件并添加一个属性来保存所有子弹:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"

@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;
@property (strong, nonatomic) SKNode *playerBullets;

@end
  1. initWithSize:levelNumber: 方法中设置 playerBullets 节点:
[self spawnEnemies];
_playerBullets = [SKNode node];
[self addChild:_playerBullets];
  1. 修改 touchesBegan:withEvent: 方法,使屏幕上半部分的点击可以发射子弹:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        if (location.y < CGRectGetHeight(self.frame) * 0.2 ) {
            CGPoint target = CGPointMake(location.x,
                                         self.playerNode.position.y);
            [self.playerNode moveToward:target];

        } else {
            BulletNode *bullet = [BulletNode
                                 bulletFrom:self.playerNode.position
                                 toward:location];
            [self.playerBullets addChild:bullet];
        }
    }
}
  1. update: 方法中调用 updateBullets 方法来更新子弹状态:
- (void)update:(CFTimeInterval)currentTime {
    [self updateBullets];
}

- (void)updateBullets {
    NSMutableArray *bulletsToRemove = [NSMutableArray array];
    for (BulletNode *bullet in self.playerBullets.children) {
        if (!CGRectContainsPoint(self.frame, bullet.position)) {
            [bulletsToRemove addObject:bullet];
            continue;
        }
        [bullet applyRecurringForce];
    }
    [self.playerBullets removeChildrenInArray:bulletsToRemove];
}

运行应用程序,你会发现除了移动玩家的飞船,还可以通过点击屏幕发射向上的导弹。

4. 使用物理引擎攻击敌人

目前游戏还缺少一些重要的游戏元素,比如敌人不会攻击我们,我们也不能通过射击消灭敌人。现在我们要解决后一个问题,让射击敌人能将其从屏幕上击落。

4.1 为节点添加物理体

EnemyNode.m 中添加物理体:

#import "PhysicsCategories.h"

- (instancetype)init {
    if (self = [super init]) {
        self.name = [NSString stringWithFormat:@"Enemy %p", self];
        [self initNodeGraph];
        [self initPhysicsBody];
    }
    return self;
}

- (void)initPhysicsBody {
    SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
                           CGSizeMake(40, 40)];
    body.affectedByGravity = NO;
    body.categoryBitMask = EnemyCategory;
    body.contactTestBitMask = PlayerCategory|EnemyCategory;
    body.mass = 0.2;
    body.angularDamping = 0.0f;
    body.linearDamping = 0.0f;
    self.physicsBody = body;
}

PlayerNode.m 中添加物理体:

#import "PhysicsCategories.h"

- (instancetype)init {
    if (self = [super init]) {
        self.name = [NSString stringWithFormat:@"Player %p", self];
        [self initNodeGraph];
        [self initPhysicsBody];
    }
    return self;
}

- (void)initPhysicsBody {
    SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
                           CGSizeMake(20, 20)];
    body.affectedByGravity = NO;
    body.categoryBitMask = PlayerCategory;
    body.contactTestBitMask = EnemyCategory;
    body.collisionBitMask = 0;

    self.physicsBody = body;
}

运行应用程序,你会发现子弹现在可以将敌人击飞到太空中,但当唯一的敌人被击走后,游戏就卡住了,所以需要添加关卡管理。

5. 关卡管理
5.1 跟踪敌人状态

添加 updateEnemies 方法来移除离开屏幕的敌人:

- (void)updateEnemies {
    NSMutableArray *enemiesToRemove = [NSMutableArray array];
    for (SKNode *node in self.enemies.children) {
        if (!CGRectContainsPoint(self.frame, node.position)) {
            [enemiesToRemove addObject:node];
            continue;
        }
    }
    if ([enemiesToRemove count] > 0) {
        [self.enemies removeChildrenInArray:enemiesToRemove];
    }
}

修改 update: 方法:

- (void)update:(CFTimeInterval)currentTime {
    if (self.finished) return;

    [self updateBullets];
    [self updateEnemies];
    [self checkForNextLevel];
}

添加 checkForNextLevel 方法:

- (void)checkForNextLevel {
    if ([self.enemies.children count] == 0) {
        [self goToNextLevel];
    }
}
5.2 过渡到下一关

实现 goToNextLevel 方法:

- (void)goToNextLevel {
    self.finished = YES;

    SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
    label.text = @"Level Complete!";
    label.fontColor = [SKColor blueColor];
    label.fontSize = 32;
    label.position = CGPointMake(self.frame.size.width * 0.5,
                                 self.frame.size.height * 0.5);
    [self addChild:label];

    GameScene *nextLevel = [[GameScene alloc]
                                initWithSize:self.frame.size
                                levelNumber:self.levelNumber + 1];
    nextLevel.playerLives = self.playerLives;
    [self.view presentScene:nextLevel
   transition:[SKTransition flipHorizontalWithDuration:1.0]];
}

运行应用程序并完成一个关卡,你会看到关卡过渡效果。

6. 自定义碰撞处理

目前游戏虽然可以玩,但缺乏挑战性。我们要让敌人在被碰撞时掉落,并且被掉落的敌人击中会使玩家失去一条生命。同时,解决子弹击中敌人后奇怪的轨迹问题。

GameScene.m 中实现碰撞处理:
1. 在类扩展声明中添加代理协议声明:

@interface GameScene () <SKPhysicsContactDelegate>
  1. initWithSize:levelNumber: 方法中配置物理世界:
self.physicsWorld.gravity = CGVectorMake(0, -1);
self.physicsWorld.contactDelegate = self;
  1. 实现 didBeginContact: 方法:
- (void)didBeginContact:(SKPhysicsContact *)contact {
    if (contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask) {
        SKNode *nodeA = contact.bodyA.node;
        SKNode *nodeB = contact.bodyB.node;

        // What do we do with these nodes?
    } else {
        SKNode *attacker = nil;
        SKNode *attackee = nil;

        if (contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask) {
            attacker = contact.bodyA.node;
            attackee = contact.bodyB.node;
        } else {
            attacker = contact.bodyB.node;
            attackee = contact.bodyA.node;
        }
        if ([attackee isKindOfClass:[PlayerNode class]]) {
            self.playerLives--;
        }
        // What do we do with the attacker and the attackee?
    }
}

目前这个方法的具体功能还未完善,仅在敌人击中玩家时减少玩家的生命数。

总结

通过以上步骤,我们逐步实现了一个基于Sprite Kit的游戏,包括敌人的创建、射击功能、关卡管理和碰撞处理等。整个开发过程涉及到多个类的创建和修改,以及物理引擎的使用。以下是整个开发流程的mermaid流程图:

graph TD;
    A[创建EnemyNode类] --> B[将敌人添加到场景中];
    B --> C[实现射击功能];
    C --> D[为节点添加物理体];
    D --> E[实现关卡管理];
    E --> F[自定义碰撞处理];

同时,为了更清晰地展示各个类的主要方法和功能,我们可以用表格呈现:
| 类名 | 主要方法 | 功能 |
| ---- | ---- | ---- |
| EnemyNode | init, initNodeGraph, initPhysicsBody | 创建敌人节点,设置外观和物理体 |
| GameScene | spawnEnemies, touchesBegan, update, updateBullets, updateEnemies, checkForNextLevel, goToNextLevel, didBeginContact | 管理场景,生成敌人,处理触摸事件,更新子弹和敌人状态,关卡管理和碰撞处理 |
| BulletNode | bulletFrom, applyRecurringForce, init | 创建子弹节点,设置推力和物理体 |

通过这些步骤和代码,你可以开发出一个具有基本功能的游戏,并根据需求进一步扩展和优化。

基于Sprite Kit的游戏开发全流程指南(续)

7. 优化敌人行为

目前敌人只是简单地出现在屏幕上,被击中后飞走。为了增加游戏的趣味性,我们可以让敌人具有更复杂的行为,比如移动和攻击。

7.1 让敌人移动

EnemyNode.m 中添加移动逻辑。首先,在类扩展中添加一个属性来保存敌人的移动方向:

@interface EnemyNode ()
@property (assign, nonatomic) CGVector movementDirection;
@end

然后在 init 方法中初始化移动方向:

- (instancetype)init {
    if (self = [super init]) {
        self.name = [NSString stringWithFormat:@"Enemy %p", self];
        [self initNodeGraph];
        [self initPhysicsBody];

        // 随机初始化移动方向
        CGFloat randomX = (arc4random_uniform(2) == 0)? -1 : 1;
        CGFloat randomY = (arc4random_uniform(2) == 0)? -1 : 1;
        self.movementDirection = CGVectorMake(randomX, randomY);
    }
    return self;
}

接着添加一个方法来更新敌人的位置:

- (void)updatePosition {
    CGPoint currentPosition = self.position;
    CGFloat speed = 2; // 敌人移动速度
    currentPosition.x += self.movementDirection.dx * speed;
    currentPosition.y += self.movementDirection.dy * speed;

    // 边界检测
    CGSize sceneSize = self.scene.size;
    if (currentPosition.x < 0 || currentPosition.x > sceneSize.width) {
        self.movementDirection.dx = -self.movementDirection.dx;
    }
    if (currentPosition.y < 0 || currentPosition.y > sceneSize.height) {
        self.movementDirection.dy = -self.movementDirection.dy;
    }

    self.position = currentPosition;
}

最后,在 GameScene.m update: 方法中调用这个方法来更新所有敌人的位置:

- (void)update:(CFTimeInterval)currentTime {
    if (self.finished) return;

    [self updateBullets];
    [self updateEnemies];
    [self checkForNextLevel];

    // 更新敌人位置
    for (EnemyNode *enemy in self.enemies.children) {
        [enemy updatePosition];
    }
}
7.2 让敌人攻击

为了让敌人能够攻击玩家,我们可以让敌人发射子弹。首先,创建一个新的类 EnemyBulletNode ,类似于 BulletNode

// EnemyBulletNode.h
#import <SpriteKit/SpriteKit.h>

@interface EnemyBulletNode : SKNode
+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination;
- (void)applyRecurringForce;
@end

// EnemyBulletNode.m
#import "EnemyBulletNode.h"
#import "PhysicsCategories.h"
#import "Geometry.h"

@interface EnemyBulletNode ()
@property (assign, nonatomic) CGVector thrust;
@end

@implementation EnemyBulletNode

- (instancetype)init {
    if (self = [super init]) {
        SKLabelNode *dot = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        dot.fontColor = [SKColor redColor];
        dot.fontSize = 40;
        dot.text = @"*";
        [self addChild:dot];

        SKPhysicsBody *body = [SKPhysicsBody bodyWithCircleOfRadius:1];
        body.dynamic = YES;
        body.categoryBitMask = EnemyMissileCategory; // 新增敌人子弹类别
        body.contactTestBitMask = PlayerCategory;
        body.collisionBitMask = PlayerCategory;
        body.mass = 0.01;

        self.physicsBody = body;
        self.name = [NSString stringWithFormat:@"EnemyBullet %p", self];
    }
    return self;
}

+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination {
    EnemyBulletNode *bullet = [[self alloc] init];

    bullet.position = start;

    CGVector movement = VectorBetweenPoints(start, destination);
    CGFloat magnitude = VectorLength(movement);
    if (magnitude == 0.0f) return nil;

    CGVector scaledMovement = VectorMultiply(movement, 1 / magnitude);

    CGFloat thrustMagnitude = 100.0;
    bullet.thrust = VectorMultiply(scaledMovement, thrustMagnitude);

    return bullet;
}

- (void)applyRecurringForce {
    [self.physicsBody applyForce:self.thrust];
}
@end

EnemyNode.m 中添加发射子弹的方法:

- (void)shootAtPlayer:(PlayerNode *)player {
    EnemyBulletNode *bullet = [EnemyBulletNode bulletFrom:self.position toward:player.position];
    [self.scene addChild:bullet];
}

GameScene.m 中,让敌人定期发射子弹。可以在 update: 方法中添加以下逻辑:

- (void)update:(CFTimeInterval)currentTime {
    if (self.finished) return;

    [self updateBullets];
    [self updateEnemies];
    [self checkForNextLevel];

    // 更新敌人位置
    for (EnemyNode *enemy in self.enemies.children) {
        [enemy updatePosition];

        // 每隔一段时间让敌人发射子弹
        static CFTimeInterval lastShotTime = 0;
        if (currentTime - lastShotTime > 2.0) { // 每2秒发射一次
            [enemy shootAtPlayer:self.playerNode];
            lastShotTime = currentTime;
        }
    }
}
8. 完善碰撞处理

之前的碰撞处理逻辑只是简单地减少玩家的生命数,现在要进一步完善,比如移除被击中的敌人和子弹。

GameScene.m didBeginContact: 方法中添加以下代码:

- (void)didBeginContact:(SKPhysicsContact *)contact {
    SKPhysicsBody *firstBody;
    SKPhysicsBody *secondBody;

    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    } else {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }

    if ((firstBody.categoryBitMask & PlayerMissileCategory) != 0 &&
        (secondBody.categoryBitMask & EnemyCategory) != 0) {
        // 玩家子弹击中敌人
        [firstBody.node removeFromParent];
        [secondBody.node removeFromParent];
    } else if ((firstBody.categoryBitMask & EnemyMissileCategory) != 0 &&
               (secondBody.categoryBitMask & PlayerCategory) != 0) {
        // 敌人子弹击中玩家
        [firstBody.node removeFromParent];
        self.playerLives--;
        if (self.playerLives <= 0) {
            // 玩家死亡,游戏结束
            [self gameOver];
        }
    }
}

添加 gameOver 方法:

- (void)gameOver {
    self.finished = YES;

    SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
    label.text = @"Game Over!";
    label.fontColor = [SKColor redColor];
    label.fontSize = 32;
    label.position = CGPointMake(self.frame.size.width * 0.5,
                                 self.frame.size.height * 0.5);
    [self addChild:label];

    // 可以在这里添加重新开始游戏的逻辑
}
9. 增加游戏界面元素

为了让游戏更加直观,我们可以增加一些界面元素,比如显示玩家的生命数和当前关卡。

GameScene.m initWithSize:levelNumber: 方法中添加以下代码:

- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
    if (self = [super initWithSize:size]) {
        self.levelNumber = levelNumber;
        self.playerLives = 3; // 初始化玩家生命数

        // 创建玩家节点
        _playerNode = [PlayerNode node];
        _playerNode.position = CGPointMake(size.width * 0.5, size.height * 0.1);
        [self addChild:_playerNode];

        // 创建敌人节点容器
        _enemies = [SKNode node];
        [self addChild:_enemies];

        // 创建玩家子弹节点容器
        _playerBullets = [SKNode node];
        [self addChild:_playerBullets];

        // 显示玩家生命数
        SKLabelNode *livesLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        livesLabel.text = [NSString stringWithFormat:@"Lives: %lu", (unsigned long)self.playerLives];
        livesLabel.fontColor = [SKColor whiteColor];
        livesLabel.fontSize = 20;
        livesLabel.position = CGPointMake(20, size.height - 20);
        [self addChild:livesLabel];
        self.livesLabel = livesLabel; // 在类扩展中添加属性保存这个标签

        // 显示当前关卡
        SKLabelNode *levelLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        levelLabel.text = [NSString stringWithFormat:@"Level: %lu", (unsigned long)self.levelNumber];
        levelLabel.fontColor = [SKColor whiteColor];
        levelLabel.fontSize = 20;
        levelLabel.position = CGPointMake(size.width - 80, size.height - 20);
        [self addChild:levelLabel];
        self.levelLabel = levelLabel; // 在类扩展中添加属性保存这个标签

        [self spawnEnemies];

        self.physicsWorld.gravity = CGVectorMake(0, -1);
        self.physicsWorld.contactDelegate = self;
    }
    return self;
}

update: 方法中更新生命数和关卡标签:

- (void)update:(CFTimeInterval)currentTime {
    if (self.finished) return;

    [self updateBullets];
    [self updateEnemies];
    [self checkForNextLevel];

    // 更新敌人位置
    for (EnemyNode *enemy in self.enemies.children) {
        [enemy updatePosition];

        // 每隔一段时间让敌人发射子弹
        static CFTimeInterval lastShotTime = 0;
        if (currentTime - lastShotTime > 2.0) { // 每2秒发射一次
            [enemy shootAtPlayer:self.playerNode];
            lastShotTime = currentTime;
        }
    }

    // 更新生命数标签
    self.livesLabel.text = [NSString stringWithFormat:@"Lives: %lu", (unsigned long)self.playerLives];
    // 更新关卡标签
    self.levelLabel.text = [NSString stringWithFormat:@"Level: %lu", (unsigned long)self.levelNumber];
}

总结与展望

通过以上步骤,我们对游戏进行了多方面的优化,包括敌人行为、碰撞处理和界面元素。整个开发过程可以用以下mermaid流程图表示:

graph TD;
    A[优化敌人行为] --> B[完善碰撞处理];
    B --> C[增加游戏界面元素];
    C --> D[游戏完成并可进一步扩展];

同时,为了更清晰地展示各个类的新增方法和功能,我们更新表格如下:
| 类名 | 新增主要方法 | 功能 |
| ---- | ---- | ---- |
| EnemyNode | updatePosition, shootAtPlayer | 让敌人移动和攻击玩家 |
| EnemyBulletNode | bulletFrom, applyRecurringForce, init | 创建敌人子弹节点,设置推力和物理体 |
| GameScene | didBeginContact(完善), gameOver, 界面元素相关代码 | 完善碰撞处理,处理游戏结束,增加界面元素 |

现在游戏已经具备了更丰富的功能和更好的用户体验。未来,你可以进一步扩展游戏,比如增加更多的关卡、不同类型的敌人和武器,或者添加音效和动画效果,让游戏更加精彩。通过不断地学习和实践,你可以开发出更加复杂和有趣的游戏。

通过短时倒谱(Cepstrogram)计算进行时-倒频分析研究(Matlab代码实现)内容概要:本文主要介绍了一项关于短时倒谱(Cepstrogram)计算在时-倒频分析中的研究,并提供了相应的Matlab代码实现。通过短时倒谱分析方法,能够有效提取信号在时间与倒频率域的特征,适用于语音、机械振动、生物医学等领域的信号处理与故障诊断。文中阐述了倒谱分析的基本原理、短时倒谱的计算流程及其在实际工程中的应用价值,展示了如何利用Matlab进行时-倒频图的可视化与分析,帮助研究人员深入理解非平稳信号的周期性成分与谐波结构。; 适合人群:具备一定信号处理基础,熟悉Matlab编程,从事电子信息、机械工程、生物医学或通信等相关领域科研工作的研究生、工程师及科研人员。; 使用场景及目标:①掌握倒谱分析与短时倒谱的基本理论及其与傅里叶变换的关系;②学习如何用Matlab实现Cepstrogram并应用于实际信号的周期性特征提取与故障诊断;③为语音识别、机械设备状态监测、振动信号分析等研究提供技术支持与方法参考; 阅读建议:建议读者结合提供的Matlab代码进行实践操作,先理解倒谱的基本概念再逐步实现短时倒谱分析,注意参数设置如窗长、重叠率等对结果的影响,同时可将该方法与其他时频分析方法(如STFT、小波变换)进行对比,以提升对信号特征的理解能力。
先看效果: https://pan.quark.cn/s/aceef06006d4 OJBetter OJBetter 是一个 Tampermonkey 脚本项目,旨在提升你在各个在线评测系统(Online Judge, OJ)网站的使用体验。 通过添加多项实用功能,改善网站界面和用户交互,使你的编程竞赛之旅更加高效、便捷。 ----- 简体中文 ----- 安装 主要功能 安装脚本,你可以获得: 黑暗模式支持:为网站添加黑暗模式,夜晚刷题不伤眼。 网站本地化:将网站的主要文本替换成你选择的语言。 题目翻译:一键翻译题目为目标语言,同时确保不破坏 LaTeX 公式。 Clist Rating 分数:显示题目的 Clist Rating 分数数据。 快捷跳转:一键跳转到该题在洛谷、VJudge 的对应页面。 代码编辑器:在题目页下方集成 Monaco 代码编辑器,支持自动保存、快捷提交、在线测试运行等功能。 一些其他小功能…… [!NOTE] 点击 网页右上角 的 按钮,即可打开设置面板, 绝大部分功能均提供了帮助文本,鼠标悬浮在 ”? 图标“ 上即可查看。 使用文档 了解更多详细信息和使用指南,请访问 Wiki 页面。 如何贡献 如果你有任何想法或功能请求,欢迎通过 Pull Requests 或 Issues 与我们分享。 改善翻译质量 项目的非中文版本主要通过机器翻译(Deepl & Google)完成,托管在 Crowdin 上。 如果你愿意帮助改进翻译,使其更准确、自然,请访问 Crowdin 项目页面 贡献你的力量。 支持其他OJ? 由于作者精力有限,并不会维护太多的类似脚本, 如果你有兴趣将此脚本适配到其他在线评测系统,非常欢迎,你只需要遵守 GP...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值