我最大的困难是
Player
和Enemy
类之间的关系以及它们与Creature类之间的关系。诸如roleToNumber()
之类的方法似乎也不是最好的处理方法,但我不确定如何以更简单的方式实现类似系统。这是处理此类的类输入和输出以及创建相遇和战斗阶段:
import java.util.Scanner;
public class MyProgram extends ConsoleProgram
{
public void run()
{
Scanner readName = new Scanner(System.in);
System.out.print("Enter your name: ");
String userName = readName.nextLine();
Scanner readRole = new Scanner(System.in);
System.out.print("Choose your role (Fighter, Ranger, Arcanist): ");
String userRole = readRole.nextLine();
while(true){
if(userRole.equalsIgnoreCase("Fighter") || userRole.equalsIgnoreCase("Ranger") || userRole.equalsIgnoreCase("Arcanist")){
break;
}else{
System.out.println("Choose a valid role");
readRole = new Scanner(System.in);
System.out.print("Choose your role (Fighter, Ranger, Arcanist): ");
userRole = readRole.nextLine();
}
}
//a demo of all of the systems
System.out.println("");
Player player = new Player(userName, userRole);
scene(player, "a mansion");
if(!player.isDead()){
scene(player, "a rock");
}
}
public String attack(Creature one, Creature two){
int a = one.attack(two);
return one.getName() + " hit " + two.getName() + " for " + a + " damage.";
}
public void battle(Player one, Creature two){
System.out.println(one);
System.out.println(two);
while(true){
Scanner readChoice = new Scanner(System.in);
System.out.print("\nWhat do you want to do (Attack, Run, Status, Use Potion): ");
String userChoice = readChoice.nextLine();
while(true){
if(!userChoice.equalsIgnoreCase("Status") && !userChoice.equalsIgnoreCase("Run") && !userChoice.equalsIgnoreCase("Attack") && !userChoice.equalsIgnoreCase("Use Potion")){
System.out.println("Choose a valid choice");
readChoice = new Scanner(System.in);
System.out.print("\nWhat do you want to do (Attack, Run, Status, Use Potion): ");
userChoice = readChoice.nextLine();
}else{
break;
}
}
if(userChoice.equalsIgnoreCase("Status")){
System.out.println(one.status());
continue;
}
if(userChoice.equalsIgnoreCase("Use Potion")){
System.out.println(one.useHealthPotion());
System.out.println(one.status());
continue;
}
if(userChoice.equalsIgnoreCase("Run")){
int run = (int)(Math.random() * 100 + 1);
if(run >= 50){
System.out.println("You successfully run.");
break;
}else{
System.out.println("You fail at running.");
}
}else if(userChoice.equalsIgnoreCase("Attack")){
System.out.println(attack(one, two));
System.out.println(two.status());
}
if(!two.isDead()){
System.out.println(attack(two, one));
System.out.println(one.status());
if(one.isDead()){
System.out.println("You died!");
break;
}
}else{
System.out.println("You killed " + two.getName() + "\n");
System.out.println("You gained " + one.gainXp(two) + " exp");
if(one.checkXp()){
System.out.println("You leveled up, your health is restored!");
System.out.println("You have " + one.getXp() + " exp");
}else{
System.out.println("You have " + one.getXp() + " exp");
}
System.out.println(one + "\n");
break;
}
}
}
public void scene(Player one, String description){
System.out.println(one.getName() + " arrives at " + description);
int x = (int)(Math.random() * 3 + 1);
for(int i = 0; i < x; i++){
if(one.isDead()){
break;
}
Enemy randEnemy = new Enemy(one.getLevel());
System.out.println("\nYou encounter " + randEnemy.getName() + " the " + randEnemy.getRole());
battle(one, randEnemy);
}
}
}
此
Creature
类具有在Players
和Enemies
之间共享的基本功能:public class Creature{
public String name;
public String role;
public int maxHp;
public int maxAtt;
public int minAtt;
public int level;
public int curHp;
public Creature(String name, String role){
this.name = name;
this.role = role;
}
public int attack(Creature other){
int att = (int)(Math.random() * (this.maxAtt - this.minAtt + 1) + this.minAtt);
other.curHp -= att;
return att;
}
public boolean isDead(){
if(this.curHp <= 0){
return true;
}else{
return false;
}
}
public int getCurHp(){
return curHp;
}
public void setCurHp(int hp){
if(hp >= maxHp - curHp){
curHp = maxHp;
}else{
curHp = hp;
}
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getRole(){
return role;
}
public void setRole(String role){
this.role = role;
}
public int getMaxHp(){
return maxHp;
}
public int getLevel(){
return level;
}
public String status(){
return name + " has " + curHp + "/" + maxHp + " health.";
}
public String toString(){
return name + " the " + role + " is level " + level + " with " + curHp + "/" + maxHp + " HP and an attack of " + maxAtt + "-" + minAtt;
}
}
这是
Player
的类:public class Player extends Creature{
public int xp;
private int hpPotions = 3;
public Player(String name, String role){
super(name, role);
this.level = 1;
rollStats();
this.curHp = maxHp;
}
public String useHealthPotion(){
if(hpPotions >= 1 ){
this.setCurHp(this.getCurHp() + 25);
hpPotions--;
return hpPotions + " potions left.";
}else{
return "No potions to use.";
}
}
public int getHealthPotion(){
return hpPotions;
}
public void setHealthPotions(int newHpPotions){
hpPotions = newHpPotions;
}
public int gainXp(Creature other){
int x = other.getLevel();
int gainedXp = x * (int)(Math.random() * (60 - 21) + 20);
xp += gainedXp;
return gainedXp;
}
public boolean checkXp(){
if(xp >= level * 40){
xp = xp - (level * 40);
levelUp();
return true;
}else{
return false;
}
}
public String status(){
return name + " has " + curHp + "/" + maxHp + " health.";
}
public String getXp(){
return xp + "/" + (level * 40);
}
//rolling for intitial stats
public void rollStats(){
int hp = 0;
int att = 0;
switch(roleToNumber()){
case 1: hp = 16; att = 10; break;
case 2: hp = 13; att = 13; break;
case 3: hp = 12; att = 14; break;
}
maxHp = (roll(6) + hp);
maxAtt = (roll(6) + att);
minAtt = (maxAtt - 3);
}
private int roll(int sides){
int aRoll = (int)(Math.random() * sides + 1);
return aRoll;
}
//Changes the inputed role to a number
private int roleToNumber(){
if(role.equalsIgnoreCase("Fighter")){
return 1;
}else if(role.equalsIgnoreCase("Ranger")){
return 2;
}else if(role.equalsIgnoreCase("Arcanist")){
return 3;
}else{
return 0;
}
}
//coding for level up with modifiers based on role
public void levelUp(){
level++;
int hp = 0;
int att = 0;
switch(roleToNumber()){
case 1: hp = 24; att = 14; break;
case 2: hp = 19; att = 19; break;
case 3: hp = 16; att = 22; break;
}
maxHp += (hp * Math.random()/2 + .7);
maxAtt += (att * Math.random()/2 + .7);
minAtt = maxAtt - 3;
this.curHp = maxHp;
}
}
这是
Enemy
的类:public class Enemy extends Creature{
public Enemy(int leveled){
super("Filler", "Filler");
this.level = 1;
this.setName(randomName());
this.setRole(randomRole());
rollStats();
if(leveled > 1){
for(int i = 1; i < leveled; i++){
levelUp();
}
}
this.curHp = maxHp;
}
//pulls a random name from an array
public String randomName(){
String[] names = {"Spooky", "Scary", "Yup"};
int index = (int)(Math.random() * names.length);
return names[index];
}
//pulls a random role from an array, these are pased to roleToNumber
public String randomRole(){
String[] roles = {"Orc", "Goblin", "Dragon"};
int index = (int)(Math.random() * roles.length);
return roles[index];
}
public void rollStats(){
int hp = 0;
int att = 0;
switch(roleToNumber()){
case 1: hp = 16; att = 10; break;
case 2: hp = 13; att = 13; break;
case 3: hp = 12; att = 14; break;
}
maxHp = (roll(6) + hp);
maxAtt = (roll(6) + att);
minAtt = (maxAtt - 3);
}
private int roll(int sides){
int aRoll = (int)(Math.random() * sides + 1);
return aRoll;
}
private int roleToNumber(){
if(role.equalsIgnoreCase("Orc")){
return 1;
}else if(role.equalsIgnoreCase("Goblin")){
return 2;
}else if(role.equalsIgnoreCase("Dragon")){
return 3;
}else{
return 0;
}
}
public void levelUp(){
level++;
int hp = 0;
int att = 0;
switch(roleToNumber()){
case 1: hp = 24; att = 14; break;
case 2: hp = 19; att = 19; break;
case 3: hp = 16; att = 22; break;
}
maxHp += (hp * Math.random()/2 + .5);
maxAtt += (att * Math.random()/2 + .5);
minAtt = maxAtt - 3;
this.curHp = maxHp;
}
}
#1 楼
首先,让我说:“干得好。”对于学习中的人来说,这段代码是可以的。不用了,现在我将其撕成碎片。 ;->
让我们忽略您的主程序,该程序有其自身的一系列问题,而是根据您的要求专注于类层次结构。
使用的基本功能OOP
我注意到的第一件事是,您并不是真的在“做”正确的类。例如,考虑一下
Creature
:public class Creature{
public String name;
public String role;
public int maxHp;
public int maxAtt;
public int minAtt;
public int level;
public int curHp;
public Creature(String name, String role){
this.name = name;
this.role = role;
}
你应该了解的有关类/对象的一件事:当构造一个类的实例时,它应该准备就绪!很少有例外,所有这些都很尴尬。
在您的
Creature
中,您正在违反此规则。您的构造函数设置name
和role
并保留所有其他实例数据不变。有几种解决方法。最基本的方法是将所有实例数据作为参数传递,或基于参数进行计算。例如,
currHp = maxHp
将是一个计算,而将maxHp
作为构造函数参数传递给它。或者,您可以依赖某些(子)类特定的方法来返回所需的值:
maxHp = getMaxHp()
不要使用
case
,请使用子类如果您正在编写OO代码,并且发现自己使用了case语句,则很有可能需要一个某种。并非总是如此-您的方法可能会打开某种外部数据-但是,如果要打开内部数据来更改行为,则可能使用子类来获得该结果。
,您将打开
roleToNumber()
,这非常昂贵,因为您每次都在不缓存结果的情况下重新运行纯函数。而不是这样做,而是创建子类。将大多数“真实”代码推送到
Enemy
和Player
类中,并使用class Orc extends Enemy
提供游戏所需的字符串描述,数值统计信息和任何特殊的攻击文本:class Orc
extends Enemy
{
public Orc() {
super(randomName(), "Orc");
}
public getMaxHp() { return roll(6) + 16; }
public getMaxAttack() { return roll(6) + 10; }
}
您也可以对播放器类执行相同的操作,除了名称不是随机的:
class Fighter
extends Player
{
public Fighter(String name)
{
super(name, "Fighter");
}
... stats ...
}
这应该使您消除
roleToNumber
以及使用它的所有内容-只需将switch
编码为直接返回正确答案的方法即可。编写实际执行您正在做的事情的方法
我注意到您陷入了编写方法的陷阱,这些方法无法执行您的代码试图执行的操作:
public String useHealthPotion(){
if(hpPotions >= 1 ){
this.setCurHp(this.getCurHp() + 25);
hpPotions--;
return hpPotions + " potions left.";
}else{
return "No potions to use.";
}
}
考虑
setCurHp()
。它有什么作用?它设置当前的Hp。很好,但是您的代码实际上在做什么?您的代码正在提高当前HP,但不能超过最大值。为什么不编写一种方法来提高当前HP,但不能超过
maxHp
?它可能被称为public raiseCurrHp(int howmuch);
,然后您可以编写:
public String useHealthPotion(){
if(hpPotions >= 1 ){
this.raiseCurrHp(25);
hpPotions--;
return hpPotions + " potions left.";
}else{
return "No potions to use.";
}
}
看起来不多,但是随着时间的推移,这种无用的减少基本getter和setter方法的每一项操作都会拖累您的代码,从而使其难以阅读和/或修改。不要害怕写您实际需要的方法,而不是写您在课堂上学到的一些基本方法。
(另一方面,
gainXp
是一个很好的方法,可以做到这一点。这样做!)评论
\ $ \ begingroup \ $
谢谢!我非常感谢您的深入答复。因此,在面向对象的编程中,可以为我希望创建的每种类型的敌人提供一个针对此类敌人扩展的类吗?我不确定在主类中创建一个新的子类是否比在特殊情况下更可取。
\ $ \ endgroup \ $
– WatCow
17年11月15日在4:26
\ $ \ begingroup \ $
我想补充一点,useHealthPotion()可能不应该返回字符串。对doSomething()的直观理解是,它不返回任何东西,或者在某些情况下,这可能是其中之一,您可以返回一个布尔值,该布尔值告诉您是否已使用了一种药水。然后,您无需打印出useHealthPotion()的结果,而是根据方法的布尔结果打印出文本。使用三元运算符的示例:`System.out.println(useHealthPotion()?hpPotions +“剩余药水。”:“没有要使用的药水。”);
\ $ \ endgroup \ $
– RaimundKrämer
17年11月15日在9:22
\ $ \ begingroup \ $
这是一个很好的答案,但是:“或者,您可以依赖某些(子)类特定的方法来返回所需的值:maxHp = getMaxHp()”通常,是的,但在构造函数中不是。这导致基类与其子类之间存在循环依赖性。构造函数不应首先调用任何方法,如果确实如此,则调用的方法应为静态,私有或最终方法。这是您原本不错的答案的主要缺陷。
\ $ \ endgroup \ $
–提莫西·卡特勒(Timothy Truckle)
17年11月15日在9:42
#2 楼
您编写了一个好的程序,其代码读起来像个好故事。在某些地方有点长,但是可以解决。您以一种易于理解的方式介绍了游戏概念(玩家,敌人)。在大多数情况下,我都同意Austin的回答。但是对于实际需要的课程数量,我有不同的看法。
目前,所有玩家的行为都相同。它们的健康点和其他一些小细节有所不同。对此建模的一种方法确实是创建子类。我更喜欢仅在行为不同的情况下创建子类,而在数据或参数不同的情况下创建子类。
一种更简单的方法是为角色定义一个
enum
。它看起来像这样:public enum PlayerRole {
Fighter, Ranger, Arcanist
}
关于枚举的一件好事是,您可以在
switch
语句中直接使用它们。这使roleToNumber
方法变得不必要。switch (player.getRole()) {
case Fighter: return 17;
case Ranger: return 23;
case Arcanist: return 20;
}
throw new IllegalStateException();
(大多数Java程序员像所有其他常量一样,都以大写形式编写枚举常量。我更喜欢使用C#混合大小写方式。)
您对
Math.random
的使用比所需的更为复杂。您应该考虑为此创建自己的类,以专注于您真正需要的随机性。我在想这样的事情:public class GameRandom {
private final Random rnd;
public GameRandom(Random rnd) {
this.rnd = rnd;
}
public int d6() {
return between(1, 6);
}
public int between(int minInclusive, int maxInclusive) {
return rnd.nextInt(1 + maxInclusive - minInclusive) + minInclusive;
}
}
关于该类的一件好事是,您可以使用由
new GameRandom(new Random(0))
创建的固定“随机”数字生成器。这对于测试代码很有用。因为在测试期间,随机性很难测试。当您的测试用例始终表现相同时,这要容易得多。多年来,Random
类一直在产生完全相同的输出,并且甚至可以保证得到保证。用户输入有一些重复的代码。典型的无效输入循环,应重试为另一种方法。可能看起来像这样:
private final Scanner input = new Scanner(System.in);
private final PrintStream output = System.out;
public <T> T choose(String prompt, T... choices) {
String choiceNames = Stream.of(choices).map(Object::toString).collect(Collectors.joining(", "));
String actualPrompt = String.format("%s (%s): ", prompt, choiceNames);
while (true) {
output.print(actualPrompt);
String answer = input.nextLine();
for (T choice : choices) {
if (answer.equalsIgnoreCase(choice.toString())) {
return choice;
}
}
output.println("Invalid choice.");
}
}
您可以使用这种方法:
String role = choose("Choose your role", "Warrior", "Archer", "Alchimist");
switch (role) {
case "Warrior": hp = 5; break;
case "Archer": hp = 8; break;
case "Alchimist": hp = 27; break;
}
甚至像这样:
PlayerRole role = choose("Choose your role", PlayerRole.values());
此代码不再出现错别字,您只需向
PlayerRole
添加新角色即可立即使用。这种方便是在Mixed Case
中而不是ALL_UPPERCASE
中写枚举常量的原因之一。可以使用
System.out.println
代替System.out.printf
。好处是您可以立即看到输出的一般模式,并且实际数据将格式化为该模式。例如:System.out.printf("%s hit %s with a %s, causing %d damage.%n", attacker, attackee, weapon, damage);
%
占位符乍看起来可能很困难,但是成千上万的人已经学会了它们,它们非常有用。评论
\ $ \ begingroup \ $
感谢您的深入回答!我喜欢您关于常见无效输入法的想法,但不确定使用String作为开关的情况是否是正确的处理方法。并且感谢您对如何实现角色的想法,我对于使用rileToNumber()处理它的方式真的不满意。
\ $ \ endgroup \ $
– WatCow
17年11月15日在12:35
\ $ \ begingroup \ $
这是一个很好的答案!我特别赞扬枚举的使用(我曾考虑过将其放入答案中,然后丢弃以阐明子类的意思)和传递专用随机对象的想法。这是一种稍微高级的模式,在OO开发(Java,C ++,C#等)中,您会发现到处都是这种模式。我对基于模板的答案有点不满意,只是因为我没有印象,OP在过程中做到了那么远。 (我可能是错的。)
\ $ \ endgroup \ $
– aghast
17年11月15日在23:32
\ $ \ begingroup \ $
@AustinHastings您是正确的,我在课程中还没走那么远:p。但是,我非常感谢您和其他人在此处发布的所有帮助。我编写此代码的预期目的不是创建游戏所必需的,而是了解如何构造类以及我应该以哪种思维方式来制作它们。
\ $ \ endgroup \ $
– WatCow
17年11月15日在23:45
\ $ \ begingroup \ $
我的输入法的第一个版本没有使用模板。但是,它有一个问题,因为我刚写完就没有编译它。当我实际尝试代码时,我不得不在几个地方进行调整。最后,它变得非常复杂,具有一些高级功能。但是由于我已经详细解释了如何使用该方法,所以我觉得还可以。该方法可能应该放在MyProgram类当前从其扩展的ConsoleProgram类中,以使其可用而无需进一步工作。
\ $ \ endgroup \ $
–罗兰·伊利格(Roland Illig)
17年11月16日在1:56
#3 楼
我正在使用面向对象程序设计用Java编写基于文本的RPG。我对编程非常陌生,目前正在通过CodeHS学习并在他们的沙箱中对其进行编码,任何帮助将不胜感激。
这是一个学习编程的很棒的早期项目。如果您对基于文本的冒险编程的最新技术感兴趣,请考虑阅读Inform7。这是一个令人惊讶的编程语言和环境。
正如我在评论中指出的那样,我告诫您在该领域中非常小心地使用OO。尝试将您的游戏规则编码为与您的游戏领域无关的类型系统的规则非常容易。
我最大的努力是与游戏规则之间的关系。玩家和敌人类别以及它们与生物类别的关系
您的直觉很明显。正确的做法至关重要,而错误的做法则会造成混乱。
在批评类层次结构时需要注意以下几点:(1)我在哪里复制代码? (2)我要切断什么设计途径?例如,玩家和敌人都在扩展生物;那很好。玩家和敌人的升级方法几乎相同,彼此无关。这是为什么?同时,显然只有玩家可以喝药水。这是为什么?
考虑退后一步,更抽象地考虑游戏领域。您的游戏必须:
跟踪剑,灯笼和袋子等游戏对象以及玩家和怪物的位置。这种见解应该告诉您,类的层次结构还不够深入。玩家与剑有一些共同点;他们可以在里面。
跟踪容器的内容,例如盒子和袋子……以及房间!房间和盒子之间的显着区别是什么?真的没什么。它们包含东西。因此,盒子与房间有一些共同之处。
解决玩家尝试的动作:如果玩家试图将剑放入包中,要么成功要么失败;无论哪种方式,都会发生一些事情。如果玩家尝试攻击吸血鬼,则会发生某些情况。动作是程序的关注点,所以也许应该有一个动作类。
解决敌人尝试的动作-并非所有NPC都是“敌人”,因此“敌人”是错误的名称。玩家和敌人有一些共同点:他们都采取在游戏状态和游戏规则中可以解决的动作。
我的意思是,现在您正在专注于将过多的特异性编码为你的系统。认真考虑一下游戏域中真正的“一种”和“可以”关系是什么,并适当地设计继承和接口层次结构。
评论
\ $ \ begingroup \ $
谢谢!我确实需要从上到下的角度看一下类,我现在才开始进入类的构建和子类的使用,因此了解所有关系一直是我一直非常期待的事情。您是否建议您提供任何资源来学习有关Java的面向对象编程的知识?我也会研究您的文章,因为我在上课时略过了它,看起来非常有趣,我只需要坐下来就能理解所有内容。再次感谢你!
\ $ \ endgroup \ $
– WatCow
17年11月15日在18:22
#4 楼
灵活性阶级敌人:我称它为NPC(非玩家角色)。您不是要让所有非玩家生物都成为敌人,对吗?是的,有些会攻击您,或者您需要攻击它们。有些您只需要利用,欺骗即可...但也许有些人实际上会提供帮助,例如提供物品或信息。否则他们可能会陪伴您执行任务。然后其他人会给您任务/寻求帮助。您将与某些人进行交易...
您希望RGB的灵活性如何?单人游戏还是多人游戏(MUD)?您已将动物定义硬编码到您的程序中,该程序将成为RPG引擎。因此,为了更改/扩展,您必须编辑实际的引擎。相反,我建议您的RPG引擎实现虚拟机。这样,编辑游戏世界通常不需要您编辑引擎。您甚至可以对正在运行的游戏进行编辑。
如果您正在制作MUD,则这些引擎存在并且可以免费下载。但是,那时您可能无法使用Java进行操作。例如,LP-MUD引擎是用C语言编写的,它允许您以称为LPC的面向对象语言定义的虚拟机形式创建非常复杂的世界。像普通玩家一样登录的管理员玩家可以在不中断世界的情况下对其进行编辑/重新编程。通常,一个管理员玩家是上帝,拥有最大的编辑权限,而其他管理员玩家则是被提升为巫师并具有或多或少有限编辑权限的非普通玩家。
评论
我写了一系列有关此空间中OO设计陷阱的博客文章,我怀疑您会陷入其中。 ericlippert.com/2015/04/27/wizards-and-warriors-part-one-思考Java类型系统是否确实是捕获游戏规则的正确位置。回复:RoleToNumber您正在犯两个错误。首先,将一组case语句的输出提供给另一组case语句(RoleToNumber方法本质上是一个case语句)。为什么不消除中间步骤?其次,您要使用case语句来实现字典的功能。
@EricLippert我会进一步询问OO是否真的必要。 @WatCow您正在尝试使用OO优化/解决什么问题?击键,代码重复(这两者都是失败的,请看您为初学者编写public x y的次数)。在进一步介绍之前,我强烈建议您阅读这篇精彩的演讲“ Stop Writing Classes”:youtu.be/5ZKvwuZSiyc您不必把它当作福音,但是值得考虑。我会问的另一个问题:Java是正确的工具吗?
@BenjaminR:您的观点是正确的,但重要的是不要将推车放在这里。如果您想做的是写很棒的文字冒险,那就别走到Inform7.com。但是,最初发布者的既定目标是学习Java的OO编程,并且我认为建议以Java的OO风格编写一个体面的文本冒险引擎是合理的。关键是要弄清楚OO概念如何在领域内正常工作,而又不会落入天真的陷阱:“剑是一种武器,因此我需要一门武器……”
@BenjaminR Eric Lippert是对的,我这样做的目的是学习面向对象编程以及在进行面向对象编程时应该采取的思维方式。我确实很欣赏也许我不必使用OO的想法,因为我确实倾向于过度复杂化那些可以通过简单函数轻松完成的事情。谢谢!