找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 210|回复: 3

[原创] 物品及物品栏系统及使用教程

[复制链接]
发表于 6 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 甲醇水溶液 于 2026-8-11 11:23 编辑

1.png
许久之前在论坛分享了物品栏的大概思路,但并不完善,且那个物品栏的用途为解谜游戏,而不是传统的“背包”系统。
因此在这分享自己写的框架以及使用教程。
首先,我们需要为物品写一个类:
[RenPy] 纯文本查看 复制代码
init -1 python:
    class Item:
        """
        基础物品类
        :item_id(str) :物品ID,每个物品应有独立且唯一的ID
        :name(str) :物品名称
        :purpose :物品用途,将在物品介绍界面显示
        :description :物品描述,将在物品介绍界面显示
        :type :物品类型,将在物品介绍界面显示
        :quantity :未使用
        :image :在物品栏时的图片路径
        :image_show :物品大图路径
        :func_throw :物品被丢弃时触发的函数,默认为"throw_item"
        :func_use :物品被使用时触发的函数,默认为"use_item"
        """
        def __init__(
            self, item_id, name, description=None,
            purpose="无",
            type="物品",
            image="",
            image_show="",
            func_throw="throw_item",
            func_use="use_item",
            ):
            self.item_id = item_id
            self.name = name
            self.purpose = purpose
            self.description = description
            self.type = type
            self.quantity = 1
            self.image = image
            self.image_show = image_show
            self.func_throw = func_throw
            self.func_use = func_use

可以注意到我添加了一些属性,它们用于在screen展示对应的信息

这里以图中的测试物品为例:
2.png
你可以这样定义一个“测试物品1”:
[RenPy] 纯文本查看 复制代码
testItem1 = Item(
        item_id="test-001",
        name="测试物品1",
        purpose="若系统正常,这个物品只能被使用而不能被丢弃",
        func_throw = None,
        description="如你所见,这是一个测试物品。它唯一的用处就是用来测试物品栏系统是否正常运转。"
    )

item_id用于在物品栏确认物品,毕竟可能存在俩个同名的物品,它用于避免这种情况
接下来,我们为物品栏写一个类,用于存放物品
[RenPy] 纯文本查看 复制代码
class PlayerHand:
        """
        玩家物品栏类
        eg: player_hand = PlayerHand()
        
        :param max_slots: 槽位总数,默认 8
        每个槽位存储格式: [item_obj, count] 或 None(空槽)
        """

        def __init__(self, max_slots=8):
            self.slots = [None] * max_slots
            self.max_slots = max_slots

        def add(self, item, count=1, slot_index=None):
            """
            放入物品。
            :param item: 物品对象
            :param count: 数量,默认 1
            :param slot_index: 指定槽位 0~7;若为None,则自动放入第一个空槽
            :return: True or False
            """
            if not hasattr(item, 'item_id'):
                print("添加失败:传入的对象缺少 item_id")
                return False

            if slot_index is not None:
                if not (0 <= slot_index < self.max_slots):
                    print(f"添加失败:槽位索引 {slot_index} 超出范围 0~{self.max_slots - 1}")
                    return False
                if self.slots[slot_index] is not None:
                    return False  # 该槽位已被占用
                self.slots[slot_index] = [item, count]
                return True
            
            # 自动寻找并堆叠
            for i in range(self.max_slots):
                if self.slots[i] is not None and item.item_id == self.slots[0].item_id:[/i]
[i]                    self.slots[1] += count[/i]
[i]                    return True[/i]

[i]            # 自动寻找第一个空槽[/i]
[i]            for i in range(self.max_slots):[/i]
[i]                if self.slots is None:[/i]
[i]                    self.slots = [item, count][/i]
[i]                    return True[/i]

[i]            print("添加失败:物品栏已满")[/i]
[i]            return False[/i]

[i]        def remove(self, slot_index, count=1):[/i]
[i]            """[/i]
[i]            从指定槽位移除指定数量的当前物品 [/i]
[i]            :return: [/i]
[i]                - 数量足够/刚好用完:返回物品对象[/i]
[i]                - 槽位为空 或 数量不够:返回 None[/i]
[i]            """[/i]
[i]            if not (0 <= slot_index < self.max_slots):[/i]
[i]                return None[/i]

[i]            slot = self.slots[slot_index][/i]
[i]            if slot is None:[/i]
[i]                return None[/i]

[i]            item, current = slot[/i]
[i]            if current < count:[/i]
[i]                return None[/i]
[i]            elif current > count:[/i]
[i]                slot[1] -= count[/i]
[i]                return item[/i]
[i]            else:[/i]
[i]                self.slots[slot_index] = None[/i]
[i]                return item[/i]

[i]        def remove_by_item(self, item, count=1):[/i]
[i]            """[/i]
[i]            按物品对象移除(遍历所有槽位)。[/i]
[i]            :return: 成功返回物品对象,失败返回 None[/i]
[i]            """[/i]
[i]            for i in range(self.max_slots):[/i]
[i]                slot = self.slots[/i]
[i]                if slot is not None and slot[0].item_id == item.item_id:[/i]
[i]                    return self.remove(i, count)[/i]
[i]            return None[/i]

[i]        def has(self, item):[/i]
[i]            """检查任意槽位中是否存在该物品"""[/i]
[i]            for i in range(self.max_slots):[/i]
[i]                if self.slots is not None and item.item_id == self.slots[0].item_id:[/i]
[i]                    return True[/i]
[i]            return False[/i]

[i]        def get(self, slot_index):[/i]
[i]            """[/i]
[i]            根据槽位索引取出物品对象(不移除)。[/i]
[i]            找不到则返回 None[/i]
[i]            """[/i]
[i]            if 0 <= slot_index < self.max_slots and self.slots[slot_index] is not None:[/i]
[i]                return self.slots[slot_index][0][/i]
[i]            return None[/i]

[i]        def find(self, item_id):[/i]
[i]            """[/i]
[i]            查找物品所在的第一个槽位索引。[/i]
[i]            :return: 槽位索引 0~7,找不到返回 -1[/i]
[i]            """[/i]
[i]            for i, slot in enumerate(self.slots):[/i]
[i]                if slot is not None and slot[0].item_id == item_id:[/i]
[i]                    return i[/i]
[i]            return -1[/i]

[i]        def is_full(self):[/i]
[i]            """是否所有槽位都已占满"""[/i]
[i]            return all(slot is not None for slot in self.slots)[/i]

[i]        def is_empty(self):[/i]
[i]            """是否所有槽位都是空的"""[/i]
[i]            return all(slot is None for slot in self.slots)[/i]

[i]        def __len__(self):[/i]
[i]            """返回已使用的槽位数"""[/i]
[i]            return sum(1 for slot in self.slots if slot is not None)[/i]

[i]        def __contains__(self, item_id):[/i]
[i]            """eg: 'key' in player_hand"""[/i]
[i]            return self.has(item_id)[/i]

[i]        def __repr__(self):[/i]
[i]            """在控制台输入定义的物品栏名即可查看"""[/i]
[i]            lines = [][/i]
[i]            for i, slot in enumerate(self.slots):[/i]
[i]                if slot is not None:[/i]
[i]                    item_obj, count = slot[/i]
[i]                    lines.append(f"[{i}] {item_obj.name} x{count}")[/i]
[i]                else:[/i]
[i]                    lines.append(f"[{i}] (空)")[/i]
[i]            return "\n".join(lines)

并不是所有的函数都将被使用,有一些函数因为需要适配我自己的游戏而被添加了进去
这里举几个简单的例子:
首先,我们定义一个物品栏
[RenPy] 纯文本查看 复制代码
smallball_hand = PlayerHand()

然后,我们在游戏里按[Shift+O]打开renpy的控制台,输入定义的物品栏名称,若先前的操作正确,你将看到以下信息:
3.png
物品栏默认为八槽,当然,可以自行修改将上限提高,把它变成一个背包。个人认为八槽物品栏最为美观。
物品栏的槽位都是空的,让我们使用add函数为物品栏添加刚刚定义的[测试物品1]
[RenPy] 纯文本查看 复制代码
smallball_hand.add(testItem1,5,4)

在控制台输入这个语句,将在物品栏的[4]号槽位添加5个测试物品1
如果你想要在物品栏移除特定的物品,可以这样做:
[RenPy] 纯文本查看 复制代码
smallball_hand.remove_by_item(testItem1,4)

这个语句将移除4个[测试物品1],
需要注意的是,如果移除的数量超过物品的当前数量,并不会将其全部移除,而是返回None。
remove_by_item运用于玩家消耗物品的情景,假设玩家需要消耗5个金币才能开门却只有4个金币,应该拒绝移除物品,而不是消耗全部的4个金币。
下面是一个物品栏界面(Screen)的例子:
[RenPy] 纯文本查看 复制代码
default bag_selected = 0[/i]
[i]screen bag_screen(hand=smallball_hand):[/i]
[i]    zorder 50[/i]
[i]    frame:[/i]
[i]        xalign 0.5[/i]
[i]        ypos 0.8[/i]
[i]        xysize(0.8,0.2)[/i]
[i]        background "#971e1e"[/i]
[i]        # 物品网格[/i]
[i]        grid 8 1:[/i]
[i]            spacing 25[/i]
[i]            xalign 0.5[/i]
[i]            yalign 0.5[/i]
[i]            for i in range(8):[/i]
[i]                $ slot = hand.slots if i < len(hand.slots) else None[/i]
[i]                button:[/i]
[i]                    xsize 150[/i]
[i]                    ysize 150[/i]
[i]                    if bag_selected == i:[/i]
[i]                        background Frame("#fff306b0", 10, 10)[/i]
[i]                    else:[/i]
[i]                        background Frame("#493e3f", 10, 10)[/i]
[i]                    hovered [[/i]
[i]                        SetVariable("bag_selected",i),[/i]
[i]                    ][/i]

[i]                    text str(i+1):[/i]
[i]                        align (1.0, 1.0)[/i]
[i]                        offset (-2, -2)[/i]
[i]                        size 25[/i]
[i]                        color "#fff"[/i]
[i]                        outlines [(1, "#000")][/i]

[i]                    if slot is not None:[/i]
[i]                        add slot[0].image:[/i]
[i]                            align (0.5, 0.5)[/i]
[i]                            at transform:[/i]
[i]                                zoom 0.8[/i]
[i]                                on hover:[/i]
[i]                                    easein 0.5 zoom 1.0[/i]
[i]                                on idle:[/i]
[i]                                    easein 0.5 zoom 0.8[/i]
[i]                    action [[/i]
[i]                        NullAction(),[/i]
[i]                    ]

如果你没有在物品栏里看到物品,可能是没有给物品定义对应的image路径。
建议在类中为物品定义默认的图片路径以方便测试
如何在screen里实现物品的“使用”和“丢弃”:
4.png
我不会在此给出完整的screen代码,因为涉及到其它不相关的代码逻辑,因此我会着重讲相关逻辑的部分
也许你已经注意到了,在物品类中有俩个属性func_throwfunc_use。它们的默认值为use_itemthrow_item
它们为函数名,当物品被使用/丢弃时将触发对应的函数
[RenPy] 纯文本查看 复制代码
init -1 python:[/i]
[i]    def use_item(item,user):[/i]
[i]        if item is None:[/i]
[i]            return False[/i]
[i]        renpy.play("audio/sound/up.wav")[/i]
[i]        return True[/i]
[i]    def throw_item(item,user):[/i]
[i]        if item is None:[/i]
[i]            return False[/i]
[i]        renpy.play("audio/sound/down.wav")[/i]
[i]        return True[/i]


[i]    def use_Handitem(hand,slot_index,user=None):[/i]
[i]        renpy.notify(f"使用")[/i]
[i]        slot = hand.slots[slot_index][/i]
[i]        if slot is None:[/i]
[i]            renpy.play("audio/sound/worse.wav")[/i]
[i]            renpy.notify(f"> 你不能使用一个不存在的东西")[/i]
[i]            return[/i]

[i]        item, count = slot[/i]

[i]        if item.func_use is None:[/i]
[i]            renpy.play("audio/sound/worse.wav")[/i]
[i]            renpy.notify(f"> [{item.name}]不能在这使用")[/i]
[i]            #该物品不能使用[/i]
[i]            return[/i]

[i]        func = getattr(renpy.store, item.func_use, None)[/i]

[i]        if func is None:[/i]
[i]            renpy.notify(f"> 错误:找不到函数 {item.func_use}")[/i]
[i]            return[/i]

[i]        # 调用函数[/i]
[i]        consumed = func(item, user)[/i]
[i]        renpy.notify(f"{consumed}")[/i]
[i]        # 如果返回 True,减少数量或移除[/i]
[i]        if consumed == True:[/i]
[i]            renpy.notify(f"{item.name} 使用成功")[/i]
[i]            hand.remove(slot_index, 1)[/i]
[i]            return[/i]
[i]        return[/i]

[i]    def throw_Handitem(hand,slot_index,user=None):[/i]

[i]        slot = hand.slots[slot_index][/i]
[i]        if slot is None:[/i]
[i]            renpy.play("audio/sound/worse.wav")[/i]
[i]            renpy.notify(f"> 这里已经一无所有了")[/i]
[i]            return[/i]

[i]        item, count = slot[/i]

[i]        if item.func_throw is None:[/i]
[i]            renpy.play("audio/sound/worse.wav")[/i]
[i]            renpy.notify(f"> [{item.name}]不能被丢弃")[/i]
[i]            #该物品不能使用[/i]
[i]            return[/i]

[i]        func = getattr(renpy.store, item.func_throw, None)[/i]

[i]        if func is None:[/i]
[i]            renpy.notify(f"> 错误:找不到函数 {item.func_throw}")[/i]
[i]            return[/i]

[i]        # 调用函数[/i]
[i]        consumed = func(item, user)[/i]
[i]        renpy.notify(f"{consumed}")[/i]
[i]        # 如果返回 True,减少数量或移除[/i]
[i]        if consumed == True:[/i]
[i]            renpy.notify(f"{item.name} 被丢弃")[/i]
[i]            hand.remove(slot_index, 1)[/i]
[i]            return[/i]
[i]        return


我在函数里使用了renpy.play用于播放音效,使用时应将路径改为你的音效路径或直接注释
renpy.notify用于在左上角显示一个消息提示。
下面是一个“使用”“丢弃”按钮的示例:
[RenPy] 纯文本查看 复制代码
textbutton "使用":
    hovered [SetVariable("bag_selected",-1),Play("sound","audio/sound/btn.wav")]
    if bag_selected == -1:
        text_color "#fff306f8"
    action [
        Function(use_Handitem, hand=hand, slot_index=curItemSlot)
    ]
textbutton "丢弃":
    hovered [SetVariable("bag_selected",-2),Play("sound","audio/sound/btn.wav")]
    if bag_selected == -2:
        text_color "#fff306f8"
    action [
        Function(throw_Handitem, hand=hand, slot_index=curItemSlot)
    ]

curItemSlot为对应物品的物品槽序号
也许你注意到了先前定义的[测试物品1]不能被丢弃。
如果你想要一个物品不能被使用/丢弃,应该在定义时把对应的func_use/func_throw设为None

[RenPy] 纯文本查看 复制代码
    testItem1 = Item(
        item_id="test-001",
        name="测试物品1",
        purpose="若系统正常,这个物品只能被使用而不能被丢弃",
        func_throw = None,
        description="如你所见,这是一个测试物品。它唯一的用处就是用来测试物品栏系统是否正常运转。"
    )

    testItem2 = Item(
        item_id="test-002",
        name="测试物品2",
        purpose="若系统正常,这个物品只能被丢弃而不能被使用",
        func_use = None,
        description="如你所见,这是一个测试物品。它唯一的用处就是用来测试物品栏系统是否正常运转。"
    )


最后,在编写物品界面的时候,也许应该考虑一个“不存在的物品”。我在写相关函数的时候考虑到了这种情况,但我尚未开始测试。



 楼主| 发表于 6 小时前 | 显示全部楼层
(你不能使用一个不存在的物品)
5.png
回复 支持 抱歉

使用道具 举报

 楼主| 发表于 6 小时前 | 显示全部楼层
本帖最后由 甲醇水溶液 于 2026-8-11 11:31 编辑

不知道为什么代码在帖子编辑后会有奇怪的 i .....
回复 支持 抱歉

使用道具 举报

发表于 半小时前 来自手机 | 显示全部楼层
本帖最后由 BuErShen 于 2026-8-11 17:25 编辑
甲醇水溶液 发表于 2026-8-11 11:29
不知道为什么代码在帖子编辑后会有奇怪的 i .....

复制粘贴时本身没有【i】【/i】?(这里用【】替代)
在 Discuz! X 帖子中【i】【/i】标签的作用是将文字设置为斜体。
回复 支持 抱歉

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|RenPy中文空间 ( 苏ICP备17067825号 )

GMT+8, 2026-8-11 18:16 , Processed in 0.019289 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表