第十六章:2D变换与坐标系

第十六章:2D变换与坐标系

"理解坐标系统是精确控制游戏对象的前提。"

在2D游戏开发中,坐标变换是基础中的基础。本章将深入讲解Godot的坐标系统、变换操作、坐标空间转换等核心概念。


16.1 坐标系统基础

16.1.1 Godot的2D坐标系

Godot 2D坐标系:
- 原点(0, 0)在左上角
- X轴向右为正
- Y轴向下为正(注意:与数学坐标系相反)

  (0,0) ────────────► X+
    │
    │
    │
    ▼
    Y+

16.1.2 坐标空间类型

# 局部坐标(Local)- 相对于父节点
# 全局坐标(Global)- 相对于场景原点
# 视口坐标(Viewport)- 相对于视口
# 屏幕坐标(Screen)- 相对于窗口

extends Node2D

func show_coordinates():
    # 局部坐标
    print("局部位置:", position)
    print("局部旋转:", rotation)
    print("局部缩放:", scale)
    
    # 全局坐标
    print("全局位置:", global_position)
    print("全局旋转:", global_rotation)
    print("全局缩放:", global_scale)

16.1.3 Transform2D

# Transform2D是2D变换的核心数据结构
# 包含:位置、旋转、缩放

extends Node2D

func understand_transform():
    # 获取变换矩阵
    var t: Transform2D = transform
    var gt: Transform2D = global_transform
    
    # Transform2D结构
    # [x轴向量, y轴向量, 原点位置]
    # x: Vector2 - X轴方向和缩放
    # y: Vector2 - Y轴方向和缩放
    # origin: Vector2 - 位置
    
    print("X轴:", t.x)
    print("Y轴:", t.y)
    print("原点:", t.origin)
    
    # 从Transform2D提取信息
    var pos = t.origin
    var rot = t.get_rotation()
    var scl = t.get_scale()

16.2 位置操作

16.2.1 设置位置

extends Node2D

func set_positions():
    # 设置局部位置
    position = Vector2(100, 200)
    position.x = 150
    position.y = 250
    
    # 设置全局位置
    global_position = Vector2(500, 300)
    
    # 相对移动
    position += Vector2(10, 0)  # 向右移动10像素
    
    # 移动到目标
    var target = Vector2(200, 200)
    position = position.move_toward(target, 5.0)  # 每次移动5像素

16.2.2 平滑移动

extends Node2D

var target_position: Vector2

func _process(delta: float):
    # 线性插值移动
    position = position.lerp(target_position, 0.1)
    
    # 带速度的移动
    var speed = 200.0
    var direction = position.direction_to(target_position)
    var distance = position.distance_to(target_position)
    
    if distance > 5.0:
        position += direction * speed * delta

# 使用Tween平滑移动
func move_to_smooth(target: Vector2, duration: float = 0.5):
    var tween = create_tween()
    tween.tween_property(self, "position", target, duration)
    tween.set_trans(Tween.TRANS_SINE)
    tween.set_ease(Tween.EASE_IN_OUT)

16.3 旋转操作

16.3.1 旋转基础

extends Node2D

func rotation_basics():
    # rotation使用弧度
    rotation = PI / 4  # 45度
    
    # rotation_degrees使用角度
    rotation_degrees = 45
    
    # 全局旋转
    global_rotation = PI / 2
    global_rotation_degrees = 90
    
    # 旋转速度
    rotation += 2.0 * delta  # 每秒2弧度
    
    # 常用角度转换
    var degrees = rad_to_deg(rotation)
    var radians = deg_to_rad(45)

16.3.2 朝向目标

extends Node2D

var target: Node2D

func _process(delta: float):
    if target:
        look_at_target()

# 立即朝向
func look_at_target():
    look_at(target.global_position)

# 平滑转向
func rotate_toward_target(speed: float, delta: float):
    var target_angle = global_position.angle_to_point(target.global_position)
    global_rotation = lerp_angle(global_rotation, target_angle, speed * delta)

# angle_to_point返回从当前点到目标点的角度
# 注意方向与look_at相反

# 正确的平滑朝向
func smooth_look_at(target_pos: Vector2, lerp_weight: float = 0.1):
    var direction = target_pos - global_position
    var target_rotation = direction.angle()
    rotation = lerp_angle(rotation, target_rotation, lerp_weight)

16.3.3 环绕旋转

extends Node2D

var pivot_point: Vector2 = Vector2.ZERO
var orbit_radius: float = 100.0
var orbit_speed: float = 2.0
var current_angle: float = 0.0

func _process(delta: float):
    # 环绕旋转
    current_angle += orbit_speed * delta
    
    position.x = pivot_point.x + cos(current_angle) * orbit_radius
    position.y = pivot_point.y + sin(current_angle) * orbit_radius

# 使用Transform2D旋转
func rotate_around_point(point: Vector2, angle: float):
    var offset = global_position - point
    offset = offset.rotated(angle)
    global_position = point + offset

16.4 缩放操作

16.4.1 缩放基础

extends Node2D

func scale_basics():
    # 均匀缩放
    scale = Vector2(2, 2)  # 放大2倍
    
    # 非均匀缩放
    scale = Vector2(2, 1)  # 只横向放大
    
    # 全局缩放
    global_scale = Vector2(1, 1)
    
    # 镜像(翻转)
    scale.x = -1  # 水平翻转
    scale.y = -1  # 垂直翻转

16.4.2 缩放动画

extends Sprite2D

func pulse_effect():
    var tween = create_tween().set_loops()
    tween.tween_property(self, "scale", Vector2(1.2, 1.2), 0.3)
    tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.3)

func pop_in():
    scale = Vector2.ZERO
    var tween = create_tween()
    tween.tween_property(self, "scale", Vector2(1.2, 1.2), 0.15)
    tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)

func squash_and_stretch():
    # 跳跃前压扁
    var tween = create_tween()
    tween.tween_property(self, "scale", Vector2(1.3, 0.7), 0.1)
    tween.tween_property(self, "scale", Vector2(0.7, 1.3), 0.1)  # 跳跃拉伸
    tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)

16.5 坐标空间转换

16.5.1 局部与全局转换

extends Node2D

func coordinate_conversion():
    # 局部坐标转全局坐标
    var local_point = Vector2(10, 20)
    var global_point = to_global(local_point)
    
    # 全局坐标转局部坐标
    var global_pos = Vector2(500, 300)
    var local_pos = to_local(global_pos)
    
    # 使用Transform2D转换
    var world_point = global_transform * local_point
    var local_from_world = global_transform.affine_inverse() * world_point

16.5.2 视口坐标转换

extends Node2D

func viewport_conversion():
    # 获取鼠标位置
    var mouse_screen = get_viewport().get_mouse_position()  # 视口坐标
    var mouse_world = get_global_mouse_position()  # 世界坐标
    var mouse_local = get_local_mouse_position()   # 局部坐标
    
    # 考虑相机变换
    var camera = get_viewport().get_camera_2d()
    if camera:
        var canvas_transform = get_canvas_transform()
        var world_pos = canvas_transform.affine_inverse() * mouse_screen

func screen_to_world(screen_pos: Vector2) -> Vector2:
    var canvas_transform = get_canvas_transform()
    return canvas_transform.affine_inverse() * screen_pos

func world_to_screen(world_pos: Vector2) -> Vector2:
    var canvas_transform = get_canvas_transform()
    return canvas_transform * world_pos

16.5.3 节点间坐标转换

extends Node2D

func convert_between_nodes(from_node: Node2D, to_node: Node2D, point: Vector2) -> Vector2:
    # 先转为全局坐标,再转为目标节点的局部坐标
    var global_point = from_node.to_global(point)
    return to_node.to_local(global_point)

# 示例:获取玩家在敌人视角中的位置
func get_player_relative_position() -> Vector2:
    var player = get_node("/root/Main/Player")
    return to_local(player.global_position)

16.6 方向与距离

16.6.1 方向计算

extends Node2D

var target: Node2D

func direction_calculations():
    # 获取朝向目标的方向向量(单位向量)
    var direction = global_position.direction_to(target.global_position)
    
    # 计算角度
    var angle = global_position.angle_to_point(target.global_position)
    
    # 从角度获取方向
    var dir_from_angle = Vector2.from_angle(rotation)
    
    # 常用方向常量
    var up = Vector2.UP       # (0, -1)
    var down = Vector2.DOWN   # (0, 1)
    var left = Vector2.LEFT   # (-1, 0)
    var right = Vector2.RIGHT # (1, 0)

func get_forward_direction() -> Vector2:
    # 获取当前朝向的前方向量
    return Vector2.RIGHT.rotated(rotation)

func get_right_direction() -> Vector2:
    # 获取右方向
    return Vector2.DOWN.rotated(rotation)

16.6.2 距离计算

extends Node2D

func distance_calculations():
    var target_pos = Vector2(200, 200)
    
    # 欧几里得距离
    var dist = position.distance_to(target_pos)
    
    # 平方距离(更快,用于比较)
    var dist_sq = position.distance_squared_to(target_pos)
    
    # 检查是否在范围内
    var range = 100.0
    if dist_sq < range * range:  # 使用平方距离避免开方
        print("在范围内")

# 查找最近的敌人
func find_nearest_enemy() -> Node2D:
    var enemies = get_tree().get_nodes_in_group("enemies")
    var nearest: Node2D = null
    var min_dist_sq = INF
    
    for enemy in enemies:
        var dist_sq = global_position.distance_squared_to(enemy.global_position)
        if dist_sq < min_dist_sq:
            min_dist_sq = dist_sq
            nearest = enemy
    
    return nearest

16.7 Transform2D进阶

16.7.1 创建变换

extends Node2D

func create_transforms():
    # 单位变换
    var identity = Transform2D.IDENTITY
    
    # 从角度创建
    var rotated = Transform2D(PI / 4, Vector2.ZERO)  # 旋转45度
    
    # 完整构造
    var t = Transform2D(
        Vector2(1, 0),    # X轴
        Vector2(0, 1),    # Y轴
        Vector2(100, 50)  # 原点
    )
    
    # 从位置、旋转、缩放创建
    var custom = Transform2D()
    custom = custom.scaled(Vector2(2, 2))
    custom = custom.rotated(PI / 4)
    custom.origin = Vector2(100, 100)

16.7.2 变换操作

extends Node2D

func transform_operations():
    var t = Transform2D.IDENTITY
    
    # 平移
    t = t.translated(Vector2(100, 50))
    
    # 旋转(绕原点)
    t = t.rotated(PI / 4)
    
    # 缩放(从原点)
    t = t.scaled(Vector2(2, 2))
    
    # 逆变换
    var inverse = t.affine_inverse()
    
    # 变换组合(注意顺序)
    var combined = t1 * t2  # 先应用t2,再应用t1
    
    # 应用变换到点
    var point = Vector2(10, 10)
    var transformed_point = t * point

16.7.3 变换插值

extends Node2D

var start_transform: Transform2D
var end_transform: Transform2D
var interpolation: float = 0.0

func _ready():
    start_transform = global_transform
    end_transform = Transform2D(PI / 2, Vector2(500, 300))

func _process(delta: float):
    interpolation += delta * 0.5
    interpolation = clamp(interpolation, 0.0, 1.0)
    
    global_transform = start_transform.interpolate_with(end_transform, interpolation)

16.8 实用工具类

16.8.1 坐标工具

# coordinate_utils.gd
class_name CoordinateUtils

# 将角度归一化到 [-PI, PI]
static func normalize_angle(angle: float) -> float:
    while angle > PI:
        angle -= TAU
    while angle < -PI:
        angle += TAU
    return angle

# 计算两个角度之间的最短旋转
static func shortest_angle_distance(from: float, to: float) -> float:
    var diff = fmod(to - from + PI, TAU) - PI
    return diff

# 检查点是否在矩形内
static func is_point_in_rect(point: Vector2, rect: Rect2) -> bool:
    return rect.has_point(point)

# 检查点是否在圆内
static func is_point_in_circle(point: Vector2, center: Vector2, radius: float) -> bool:
    return point.distance_squared_to(center) <= radius * radius

# 获取圆上的点
static func point_on_circle(center: Vector2, radius: float, angle: float) -> Vector2:
    return center + Vector2(cos(angle), sin(angle)) * radius

# 随机圆内点
static func random_point_in_circle(center: Vector2, radius: float) -> Vector2:
    var angle = randf() * TAU
    var r = sqrt(randf()) * radius
    return center + Vector2(cos(angle), sin(angle)) * r

# 网格对齐
static func snap_to_grid(point: Vector2, grid_size: Vector2) -> Vector2:
    return Vector2(
        snappedf(point.x, grid_size.x),
        snappedf(point.y, grid_size.y)
    )

16.8.2 路径跟随

# path_follower.gd
extends Node2D

@export var path: Path2D
@export var speed: float = 100.0
@export var loop: bool = true

var _progress: float = 0.0
var _path_follow: PathFollow2D

func _ready():
    if path:
        _setup_path_follow()

func _setup_path_follow():
    _path_follow = PathFollow2D.new()
    _path_follow.rotates = true
    _path_follow.loop = loop
    path.add_child(_path_follow)

func _process(delta: float):
    if _path_follow:
        _progress += speed * delta
        _path_follow.progress = _progress
        
        global_position = _path_follow.global_position
        global_rotation = _path_follow.global_rotation

16.9 实际案例

16.9.1 相机跟随系统

# camera_follow.gd
extends Camera2D

@export var target: Node2D
@export var smoothing: float = 5.0
@export var look_ahead: float = 50.0
@export var dead_zone: Vector2 = Vector2(20, 20)

var _target_position: Vector2

func _physics_process(delta: float):
    if not target:
        return
    
    var target_pos = target.global_position
    
    # 添加前瞻
    if target is CharacterBody2D:
        var velocity = target.velocity
        target_pos += velocity.normalized() * look_ahead
    
    # 死区处理
    var diff = target_pos - global_position
    if abs(diff.x) < dead_zone.x:
        target_pos.x = global_position.x
    if abs(diff.y) < dead_zone.y:
        target_pos.y = global_position.y
    
    # 平滑跟随
    global_position = global_position.lerp(target_pos, smoothing * delta)

16.9.2 视野检测

# vision_cone.gd
extends Area2D

@export var view_angle: float = 60.0  # 度
@export var view_distance: float = 300.0

func can_see(target: Node2D) -> bool:
    var to_target = target.global_position - global_position
    var distance = to_target.length()
    
    # 距离检查
    if distance > view_distance:
        return false
    
    # 角度检查
    var forward = Vector2.RIGHT.rotated(global_rotation)
    var angle_to_target = forward.angle_to(to_target)
    
    if abs(rad_to_deg(angle_to_target)) > view_angle / 2:
        return false
    
    # 射线检查(遮挡)
    var space = get_world_2d().direct_space_state
    var query = PhysicsRayQueryParameters2D.create(
        global_position,
        target.global_position,
        1  # collision_mask
    )
    query.exclude = [self]
    
    var result = space.intersect_ray(query)
    if result.is_empty():
        return true
    
    return result.collider == target

本章小结

本章深入学习了Godot的2D变换与坐标系统:

  1. 坐标系统基础:Godot坐标系、坐标空间类型
  2. 位置操作:设置位置、平滑移动
  3. 旋转操作:旋转基础、朝向目标、环绕旋转
  4. 缩放操作:缩放基础、缩放动画
  5. 坐标空间转换:局部/全局/视口坐标转换
  6. 方向与距离:方向计算、距离计算
  7. Transform2D进阶:创建、操作、插值
  8. 实用工具:坐标工具、路径跟随、相机跟随

下一章将学习2D物理系统。


上一章:精灵与纹理

下一章:2D物理系统

← 返回目录