Skip to content

Break Block

Let’s use Python to control the player and break surrounding blocks.

On this page, you will learn the following:

  • Quaternions
  • Service communication
  • Position P-control

1. Try Service Communication

Service communication is a synchronous/asynchronous communication method where a server and client exchange requests and responses.

In minecraft_ros2, services are used for breaking blocks, executing in-game commands, summoning entities, and more.

First, run the client and join a world.

Check the list of available services:

bash
ros2 service list

In this tutorial, we will use the /dig_block service.

Aim at a block in the player’s view and run:

bash
ros2 service call /dig_block minecraft_msgs/srv/DigBlock

If the block breaks, it worked!

2. Get the Player’s Orientation

Now you can break blocks, but you also need to aim at them. Let’s automate that.

For this tutorial, we will use debug messages to get the player’s pose.

The /player/ground_truth topic publishes the player’s position and orientation. Try:

bash
ros2 topic echo /player/ground_truth

Example output:

bash
---
position:
  x: 14.988304348096932
  y: -589.1905229054375
  z: 74.0
orientation:
  x: 0.025652511045336723
  y: 0.03142489492893219
  z: -0.6318491101264954
  w: 0.7740291357040405
---

This gives you the position (x, y, z) and orientation (x, y, z, w).

That extra w? This is called a quaternion, which represents rotation without gimbal lock.

You can also convert a quaternion into roll-pitch-yaw (Euler angles).

2. Add New Code

Create a new file at:

~/minecraft_ros2_tutorial_ws/src/minecraft_ros2_tutorial/minecraft_ros2_tutorial/break_block.py

and add the following code:

py
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist, Pose
import math

class BreakBlock(Node):
    def __init__(self):
        super().__init__('break_block')
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        self.subscription = self.create_subscription(
            Pose, '/player/ground_truth', self.pose_callback, 10
        ) # Call pose_callback when receiving /player/ground_truth

        self.player_pose = None
        self.Kp = 2.0
        self.timer = self.create_timer(0.01, self.timer_callback)
        self.get_logger().info("Break Block Node Initialized.")

    def pose_callback(self, msg: Pose):
        self.player_pose = msg

    def quaternion_to_yaw_pitch(self, q):
        x, y, z, w = q.x, q.y, q.z, q.w

        siny_cosp = 2.0 * (w * z + x * y)
        cosy_cosp = 1.0 - 2.0 * (y * y + z * z)
        yaw = -math.atan2(siny_cosp, cosy_cosp)

        sinp = 2.0 * (w * y - z * x)
        if abs(sinp) >= 1:
            pitch = math.copysign(math.pi / 2, sinp)
        else:
            pitch = math.asin(sinp)

        return yaw, pitch

    def timer_callback(self):
        if self.player_pose is None:
            return

        yaw, pitch = self.quaternion_to_yaw_pitch(self.player_pose.orientation)

        self.target_block_position = [1, 0, -1] # x: forward, y: left, z: down
        dx, dy, dz = self.target_block_position
        dz = dz - 1 # Adjust for player’s head offset
        target_yaw = math.atan2(-dy, dx)
        target_pitch = math.atan2(-dz, math.sqrt(dx**2 + (-dy)**2))

        yaw_error = target_yaw - yaw
        pitch_error = target_pitch - pitch

        twist = Twist()
        twist.angular.y = -self.Kp * pitch_error
        twist.angular.z = -self.Kp * yaw_error
        self.publisher_.publish(twist)

def main(args=None):
    rclpy.init(args=args)
    node = BreakBlock()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Add this line to the console_scripts section in setup.py:

py
'break_block = minecraft_ros2_tutorial.break_block:main',

Then, rebuild and source:

bash
colcon build
source install/setup.bash

Run the node:

bash
ros2 run minecraft_ros2_tutorial break_block

Now the player’s view automatically points to target_block_position. This is achieved using coordinate conversion and P-control.

  • target_block_position is the block’s relative position (here: forward + downward).
  • P-control means the control command is proportional to the error.

Try changing target_block_position and observe the behavior.

3. Break a Specific Block

Next, let’s break the block automatically and move forward, creating a staircase.

Since breaking blocks pauses the game, a special flag is used here.

py
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist, Pose
from minecraft_msgs.srv import DigBlock
import math

class BreakBlock(Node):
    def __init__(self):
        super().__init__('break_block')
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        self.subscription = self.create_subscription(
            Pose, '/player/ground_truth', self.pose_callback, 10
        )

        # DigBlock service client
        self.dig_client = self.create_client(DigBlock, '/dig_block')
        while not self.dig_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Waiting for /dig_block service...')

        self.player_pose = None
        self.in_progress = False
        self.Kp = 2.0
        self.timer = self.create_timer(0.1, self.timer_callback)
        self.get_logger().info("Break Block Node Initialized.")

    def pose_callback(self, msg: Pose):
        self.player_pose = msg

    def quaternion_to_yaw_pitch(self, q):
        x, y, z, w = q.x, q.y, q.z, q.w

        siny_cosp = 2.0 * (w * z + x * y)
        cosy_cosp = 1.0 - 2.0 * (y * y + z * z)
        yaw = -math.atan2(siny_cosp, cosy_cosp)

        sinp = 2.0 * (w * y - z * x)
        if abs(sinp) >= 1:
            pitch = math.copysign(math.pi / 2, sinp)
        else:
            pitch = math.asin(sinp)

        return yaw, pitch

    def timer_callback(self):
        if self.player_pose is None:
            return

        yaw, pitch = self.quaternion_to_yaw_pitch(self.player_pose.orientation)

        self.target_block_position = [1, 0, -1]
        dx, dy, dz = self.target_block_position
        target_yaw = math.atan2(-dy, dx)
        target_pitch = math.atan2(-dz, math.sqrt(dx**2 + (-dy)**2))

        yaw_error = target_yaw - yaw
        pitch_error = target_pitch - pitch

        twist = Twist()
        twist.angular.y = -self.Kp * pitch_error
        twist.angular.z = -self.Kp * yaw_error
        self.publisher_.publish(twist)

        if abs(yaw_error) < 0.05 and abs(pitch_error) < 0.05:
            if not self.in_progress:
                self.in_progress = True
                self.get_logger().info('Target reached, sending dig command...')
                twist = Twist()
                self.publisher_.publish(twist)
                req = DigBlock.Request()
                future = self.dig_client.call_async(req)
                future.add_done_callback(self.dig_response_callback)

    def dig_response_callback(self, future):
        try:
            response = future.result()
            self.get_logger().info(f'DigBlock response received: {response}')
        except Exception as e:
            self.get_logger().error(f'Service call failed: {e}')
        self.in_progress = False

        twist = Twist()
        twist.linear.x = 1.0
        self.publisher_.publish(twist)

def main(args=None):
    rclpy.init(args=args)
    node = BreakBlock()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Also, add the dependency in package.xml:

xml
<depend>minecraft_msgs</depend>

When you run this, the player will automatically dig blocks and move forward, creating a staircase!