我应该实现一个基于手势的菜单,您可以在其中通过平移或翻动来滚动项目的水平列表。这种菜单在智能手机游戏中很常见。示例案例是割绳子,您选择盒子(纸板箱,织物盒)或愤怒的小鸟,您选择关卡集(荷包蛋,Mighty Hoax)。
我在想的是,我必须进行一些复杂的物理计算,并根据手势为菜单项提供速度和加速度。有更好的解决方案吗?我正在使用 libgdx 顺便说一句。
我应该实现一个基于手势的菜单,您可以在其中通过平移或翻动来滚动项目的水平列表。这种菜单在智能手机游戏中很常见。示例案例是割绳子,您选择盒子(纸板箱,织物盒)或愤怒的小鸟,您选择关卡集(荷包蛋,Mighty Hoax)。
我在想的是,我必须进行一些复杂的物理计算,并根据手势为菜单项提供速度和加速度。有更好的解决方案吗?我正在使用 libgdx 顺便说一句。
我认为你不需要经历所有这些来实现一个简单的菜单!这一切都是关于为各种项目定义偏移量(我只是假设你想要 Cut the Rope 风格的菜单,在给定的时刻只有一个条目(不包括转换)),然后在检测到轻弹时在这些偏移量之间进行补间!
您似乎已经将手势系统全部连接起来,所以现在,我们只需要弄清楚如何显示菜单。为简单起见,我们假设我们不希望菜单环绕。
我们将首先在我们的脑海中设想这个菜单会是什么样子。它就像一张幻灯片,穿过手机,可以通过屏幕看到。
phone ("stuff" is currently selected)
========
|---------------| |-------|
| Start | About | Stuff | Quit |
|---------------| |-------|
| |
| |
| |
========
我们只是假设屏幕宽度是w
,因此,所有菜单条目都是这个宽度(再想想割绳子!)。
现在,当要显示“开始”时,我们应该只在屏幕上从第一个元素“开始”开始渲染幻灯片,而理论上,其余的将位于屏幕的右侧。这将被视为基本情况,使用offset = 0渲染菜单。
是的,是的,这个偏移量将是我们滑动小菜单的关键!Now, it's pretty obvious that when about is selected, we'll just have to offset the "filmstrip" to the left by one "frame", and here offset = - 1 * frameWidth . 我出色的 ASCII 艺术说明的示例案例选择了第三个菜单项,并且由于帧的索引从 0 开始,我们只需减去两倍的 frameWidth 并获得所需的偏移量。我们将从偏移量 = -2 * frameWidth 开始渲染菜单。
(显然,您可以提前计算 frameWidth,通过使用 API 获取屏幕宽度,然后仅绘制居中的菜单元素文本/图形)。
所以这很简单:
用户向左扫,我们需要靠近偏移量 0 的菜单,我们将所选实体的索引减一,然后菜单跳转到正确的位置
用户向右扫,我们增加索引(显然只要它不超过菜单元素的数量 - 1)
但是平滑补间呢? 幸运的是,Libgdx 已经为漂亮的小补间设置了插值。我们只需要处理一些事情,这样我们就不会射中自己的腿。我会在这里列出它们。
一个快速说明: Cut the Rope 级别选择器的工作方式与我在这里所说的略有不同。它不仅对轻弹(预定义的手势)做出反应,而且更敏感。您可能可以通过使用偏移量和跟踪手指在屏幕上的位置来实现类似的效果。(如果用户将菜单项向左/向右拖动过多,则会自动切换到上一个/下一个) 友好建议:只需设置一个简单的、有效的菜单,并将这样的细节留在最后,因为它们可能会结束花费很多时间!:P
好吧,回到正轨!
我们现在拥有的是一种在偏移量之间快速切换的方法。我们只需要补间。还有一些额外的成员在起作用,但我认为它们是不言自明的。当我们在两个元素之间转换时,我们会记住“旧”偏移量,以及我们要前往的那个偏移量,以及我们从转换中离开的时间,我们使用这四个变量来计算偏移量(在当前时刻使用 libgdx 插值,在这种情况下为 exp10),从而产生平滑的动画。
让我们看看,我已经创建了一个 quick'n'dirty mock-up。我已经尽我所能对代码进行了注释,所以我希望下面的代码片段能说明一切!:D
import java.util.ArrayList;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;
public class MenuManager {
// The list of the entries being iterated over
private ArrayList<MenuEntry> entries = new ArrayList<>();
// The current selected thingy
private int index;
// The menu offset
private float offset = 0.0f;
// Offset of the old menu position, before it started tweening to the new one
private float oldOffset = 0.0f;
// What we're tweening towards
private float targetOffset = 0.0f;
// Hardcoded, I know, you can set this in a smarter fashion to suit your
// needs - it's basically as wide as the screen in my case
private float entryWidth = 400.0f;
// Whether we've finished tweening
private boolean finished = true;
// How much time a single transition should take
private float transitionTimeTotal = 0.33f;
// How much time we have left from the current transition
private float transitionTimeLeft = 0.0f;
// libgdx helper to nicely interpolate between the current and the next
// positions
private Interpolation interpolation = Interpolation.exp10;
public void addEntry(MenuEntry entry) {
entries.add(entry);
}
// Called to initiate transition to the next element in the menu
public void selectNext() {
// Don't do anything if we're still animationg
if(!finished) return;
if(index < entries.size() - 1) {
index++;
// We need to head towards the next "frame" of the "filmstrip"
targetOffset = oldOffset + entryWidth;
finished = false;
transitionTimeLeft = transitionTimeTotal;
} else {
// (does nothing now, menu doesn't wrap around)
System.out.println("Cannot go to menu entry > entries.size()!");
}
}
// see selectNext()
public void selectPrevious() {
if(!finished) return;
if(index > 0) {
index --;
targetOffset = oldOffset - entryWidth;
finished = false;
transitionTimeLeft = transitionTimeTotal;
} else {
System.out.println("Cannot go to menu entry <0!");
}
}
// Called when the user selects someting (taps the menu, presses a button, whatever)
public void selectCurrent() {
if(!finished) {
System.out.println("Still moving, hold yer pants!");
} else {
entries.get(index).select();
}
}
public void update(float delta) {
if(transitionTimeLeft > 0.0f) {
// if we're still transitioning
transitionTimeLeft -= delta;
offset = interpolation.apply(oldOffset, targetOffset, 1 - transitionTimeLeft / transitionTimeTotal);
} else {
// Transition is over but we haven't handled it yet
if(!finished) {
transitionTimeLeft = 0.0f;
finished = true;
oldOffset = targetOffset;
}
}
}
// Todo make font belong to menu
public void draw(SpriteBatch spriteBatch, BitmapFont font) {
if(!finished) {
// We're animating, just iterate through everything and draw it,
// it's not like we're wasting *too* much CPU power
for(int i = 0; i < entries.size(); i++) {
entries.get(i).draw((int)(i * entryWidth - offset), 100, spriteBatch, font);
}
} else {
// We're not animating, just draw the active thingy
entries.get(index).draw(0, 100, spriteBatch, font);
}
}
}
而且我相信一个可以自己绘制的简单的基于文本的菜单条目就足够了!(请注意肮脏的硬编码文本换行宽度!)
public class MenuEntry {
private String label;
// private RenderNode2D graphic;
private Action action;
public MenuEntry(String label, Action action) {
this.label = label;
this.action = action;
}
public void select() {
this.action.execute();
}
public void draw(int x, int y, SpriteBatch spriteBatch, BitmapFont font) {
font.drawMultiLine(spriteBatch, label, x, y, 400, HAlignment.CENTER);
}
}
哦,Action只是一个具有执行方法的东西,并且,代表一个动作。
public interface Action {
abstract void execute();
}
随时在评论中提出任何相关问题,我将尝试澄清需要什么。希望这可以帮助!