UE4 C++ Multiple widgets created for the same actor in UI - c++

I am trying to set up a system that displays the ground loot near the player (i.e., actors that are overlapping the player's UBoxComponent). This needs to change as and when the player moves and the ground loot actors are no longer being overlapped.
I have sort of got it working - when the actors are on their own the function seems to work. However, when two ground loot actors are close to one another, the inventory displays multiple widgets for the same overlapping actor on the floor and sometimes only recognises one of the actors on the floor. It seems to be having trouble when the Player's UBoxComponent overlaps more than one actor at a time. I'll link a YouTube video to help show the issue I'm having because it's quite hard to describe: https://youtu.be/a_zMl1zOUDc
This is the function for updating the ground loot widgets. It is called by a dynamic multicast delegate that is broadcasted from the player class:
// Iterates through the ground loot around the player and displays a widget for each item
void UInventory::UpdateGroundLoot()
{
MyPlayer = Cast<AMainCharacter>(GetOwningPlayer()->GetCharacter());
if(!MyPlayer) { return; } // Null check
// Get a ref to the Player's inventory component
UInventoryComponent* PlayerInventoryComp = MyPlayer->PlayerInventory;
// Clear any current widgets in the ground loot list
GroundLootScrollBox->ClearChildren();
// GroundLootActorsArray is an array of AActors that gets added to/emptied based on AActors that overlap with the Player's UBoxComponent
if(!PlayerInventoryComp && !PlayerInventoryComp->GroundLootActorsArray.IsValidIndex(0)) { return; } // Null check
for(int32 i = 0; i < PlayerInventoryComp->GroundLootActorsArray.Num(); i++)
{
// Cast each element of the GroundLootItems array to AItem* GroundItem
AItem* GroundItem = Cast<AItem>(PlayerInventoryComp->GroundLootActorsArray[i]);
if(!GroundItem) { return; } // Null check
// Add GroundItem to a new array (GroundLootItemsArray). This is an array specifically of AItems (rather than AActors).
GroundLootItemsArray.Add(GroundItem);
GroundLootWidget = CreateWidget<UInventoryItemWidget>(GetOwningPlayer(), InventoryItemWidgetClass);
// Add each created widget to an array of widgets called GroundLootWidgetsArray
GroundLootWidgetsArray.Add(GroundLootWidget);
if(!GroundLootWidgetsArray.IsValidIndex(0) && !GroundLootItemsArray.IsValidIndex(0)) { return; } // Null check
// Display item specific names/thumbnails for each widget created
GroundLootWidgetsArray[i]->ItemName->SetText(GroundLootItemsArray[i]->ItemDisplayName);
GroundLootWidgetsArray[i]->Thumbnail->SetBrushFromTexture(GroundLootItemsArray[i]->Thumbnail);
// Add each array element of the GroundLootWidgetsArray to the scroll box in the widget blueprint
GroundLootScrollBox->AddChild(GroundLootWidgetsArray[i]);
}
}

DevilsD in the comments pointed me in the right direction here. There were two main issues I had.
The delegate that was designed to update the ground loot widgets was being broadcasted in a for loop for every element of the OverlappingActor array. I think this resulted in multiple widgets being displayed for the same actor. The code in the Player's class now looks like this (TriggerEnter and TriggerExit are called by the OnComponentBeginOverlap and OnComponentEndOverlap delegates in the constructor respectively) :
void AMainCharacter::TriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
bItemIsWithinRange = true;
// I think the TSubclassOf ensures that we only pickup AItem classes and it's children - rather than all actors on the ground
OverlappedComponent->GetOverlappingActors(OverlappingActors, TSubclassOf<AItem>(AItem::StaticClass()));
if(PlayerInventory && OverlappingActors.IsValidIndex(0))
{
for(auto& GroundItems : OverlappingActors)
{
PlayerInventory->AddItemToGroundLoot(GroundItems);
}
OnUpdateGroundLoot.Broadcast();
}
}
void AMainCharacter::TriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
bItemIsWithinRange = false;
if(PlayerInventory && OverlappingActors.IsValidIndex(0))
{
for(auto& GroundItems : OverlappingActors)
{
PlayerInventory->GroundLootActorsArray.Remove(GroundItems);
}
OnUpdateGroundLoot.Broadcast();
OverlappingActors.Empty();
}
}
I wasn't emptying the GroundLootItemsArray every time I was updating the ground loot and so previous ground loot was being stored in this array on each new update. This also resulted in showing the same widget for two different actors. I have now set it to empty along with clearing the children of the scroll box before any widget creation takes place in the UInventory.cpp (which is a child of UUserWidget):
// Iterates through the ground loot around the player and displays a widget for each item
void UInventory::UpdateGroundLoot()
{
MyPlayer = Cast<AMainCharacter>(GetOwningPlayer()->GetCharacter());
if(!MyPlayer) { return; } // Null check
// Get a ref to the Player's inventory component
UInventoryComponent* PlayerInventoryComp = MyPlayer->PlayerInventory;
// Clear any current widgets in the ground loot list
GroundLootScrollBox->ClearChildren();
// Empties this array to prevent replication of Item specific information that gets sent to the widget (i.e., the thumbnail or the name of the item)
GroundLootItemsArray.Empty();
// GroundLootActorsArray is an array of AActors that gets added to/emptied based on AActors that overlap with the Player's UBoxComponent
if(!PlayerInventoryComp && !PlayerInventoryComp->GroundLootActorsArray.IsValidIndex(0)) { return; } // Null check
for(int32 i = 0; i < PlayerInventoryComp->GroundLootActorsArray.Num(); i++)
{
// Cast each element of the GroundLootItems array to AItem* GroundItem
AItem* GroundItem = Cast<AItem>(PlayerInventoryComp->GroundLootActorsArray[i]);
if(!GroundItem) { return; } // Null check
// Add GroundItem to a new array (GroundLootItemsArray). This is an array specifically of AItems (rather than AActors).
GroundLootItemsArray.AddUnique(GroundItem);
GroundLootWidget = CreateWidget<UInventoryItemWidget>(GetOwningPlayer(), InventoryItemWidgetClass);
// Add each created widget to an array of widgets called GroundLootWidgetsArray
GroundLootWidgetsArray.AddUnique(GroundLootWidget);
if(!GroundLootWidgetsArray.IsValidIndex(0) && !GroundLootItemsArray.IsValidIndex(0)) { return; } // Null check
// Display item specific names/thumbnails for each widget created
GroundLootWidgetsArray[i]->ItemName->SetText(GroundLootItemsArray[i]->ItemDisplayName);
GroundLootWidgetsArray[i]->Thumbnail->SetBrushFromTexture(GroundLootItemsArray[i]->Thumbnail);
// Add each array element of the GroundLootWidgetsArray to the scroll box in the widget blueprint
GroundLootScrollBox->AddChild(GroundLootWidgetsArray[i]);
}
}
I also changed the adding of actors/items to the arrays from Array.Add(Actor) to Array.AddUnique(Actor). I'm not sure if this was necessary but it made sense to me because I didn't want more than one of any given item accidently being added to the array.

Related

First Visible Row in Codenameone

I tried this code to get the first visible row in a scrolling Table inside a BorderLayout.CENTER, but it didn't work, seems the points returned do not reflect the visible cells, unless I am missing a sort of calculation,
thank you for your insights,
#Override
protected void onScrollY(int scrollY) {
super.onScrollY(scrollY); //To change body of generated methods, choose Tools | Templates.
Component c=getComponentAt(50, scrollY);
if (c instanceof Table){
System.err.println("table "+getWidth()+" "+getHeight()+" s "+scrollY);
return;
}
Button b=(Button) c;
System.err.println("c: "+b.getText());
}
getComponentAt(x,y) takes absolute (screen) coordinates. The scrollY value is a relative coordinate in that container.
So what you want is something like:
Component c = getComponentAt(getAbsoluteX()+50, getAbsoluteY() + scrollY)
Also worth nothing that getComponentAt(x,y) will only return components that are focusable or have been set to grab pointer events. If you just want to find the first paintable immediate child of this container, and you're using a BoxLayout.Y_AXIS layout, then you might be better to just iterate through the children until you find one where y is at least the scrollY.
e.g.
Component c = null;
for (Component child : this) {
if (child.getY() + child.getHeight() > scrollY) {
c = child;
break;
}
}
....

Array receives two of the same values

Sorry if the code is sloppy, I've only been working with QT for the past couple of weeks. I'm working on a small game while studying, and right now I'm working on when the player buys an item from the shop it will be placed it into the character bag.
The Problem is when I buy the item once, it works fine. But if I delete the item after purchase then repurchase the same item again it will put two of the same item into the array even though I purchased one.
I have 3 files used for this. Inventory.cpp, GameScreen.cpp, Shops.cpp.
GameScreen.cpp is the main file. Here I just initialize everything.
void GameScreen::initGame(QString &characterName, QString &characterProfession){
//PASS CHARACTERNAME AND CHARACTERPROFESSION INTO INVENTORY
inv.initCharacter(characterName, characterProfession);
//INIT BAG
inv.initBag();
//INIT MONEY
inv.initMoney();
//SHOPS INITS
mos.initShop();
mos.passMoneyToShop(inv.gold, inv.silver, inv.copper);
}
This will open the shop window. The Item connect is apart of my problem.
void GameScreen::on_mapOneShopB_clicked()
{
mos.setModal(true);
//SEND THE ITEM INTO BAG
connect(&mos, SIGNAL(getItemFromMapOneShop(const QString&)), &inv, SLOT(bagAddElement(const QString&)));
mos.show();
mos.exec();
}
Moving into Shops.cpp. I use polymorphism in this file and that's the reason why you see MapOneShop:: instead of Shops::. Sorry about the confusion. But moving on, I hit the button and it subtracts the item price from my amount of gold.
void MapOneShop::on_buyB_clicked()
{
//ONLY WORKS IF I HIT THE ITEM NAME COLUMN THEN HIT BUY
gold -= itemPrice[ui->treeWidget->currentColumn()];
//UPDATE INFORMATION
updateInformationVAndMoneyAfterBuy();
}
It then moves into updateInformationVAndMoneyAfterBuy(); which checks that I spent money, and If I did it will emit the item I need. I just emit back to GameScreen.cpp inside of void GameScreen::on_mapOneShopB_clicked() and pass them into Inventory.cpp.
void MapOneShop::updateInformationVAndMoneyAfterBuy()
{
//UPDATE INFORMATIONV FIRST TO CHECK MONEY CHANGES
if(goldCheck != gold)
{
emit getUpdatedMoneyFromShop(gold, silver, copper);
//ITEM NAME IS WHERE I STORE THE NAMES OF THE ITEMS
emit getItemFromMapOneShop(this->itemName[ui->treeWidget->currentColumn()]);
}
}
Now moving into Inventory.cpp. When the item gets passed into bagAddElement after deleting then repurchasing, I get two of the same items even though only 1 should be passed in. I am using a dynamic array for this. Below I will also show the void Inventory::on_deleteB_clicked, void Inventory::bagDeleteAt and also void Inventory::bagLWPrint functions. The items value is my array holding the shopItem strings.
void Inventory::bagAddElement(const QString& shopItem)
{
//I USE THIS TO CHECK THE VALUE shopItem.
qDebug()<<"bagAddElement: " << shopItem;
//IF THE CURRENT POSITION IN THE BAG
//IS BIGGER THAN THE CURRENT SIZE
//IT WILL INCREASE THE BAG FOR US
if(nrOfEl >= bagSize)
{
bagExpand();
}
//CHECK FOR VALUES INSIDE ARRAY AND CHECK IF THEY ARE NULL
//IF SO, IT WILL ADD THE ITEM INTO THE NULL POSITION
for(int i = 0; i < bagSize; i++)
{
if(items[i] == nullptr)
{
nrOfEl = i;
items[nrOfEl++] = shopItem;
break;
}
}
//UPDATE WIDGET LIST
bagLWPrint();
}
Here I pass the currently selected row value inside bagLW and pass it into void Inventory::bagDeleteAt.
void Inventory::on_deleteB_clicked()
{
//BagLW IS A LIST WIDGET
bagDeleteAt(bagLW->currentRow());
}
Now we look for the what's inside items[row] and set it to nullptr. Then we go into void Inventory::bagLWPrint.
void Inventory::bagDeleteAt(int row)
{
if(items[row] == nullptr)
{
//IF THE ITEM IS ALREADY NULL WILL PRINT A MESSAGE
QMessageBox::information(this,"Bag","No item in that slot");
}
else
{
//SET CURRENTLY SELECTED ITEM TO NULL
items[row] = nullptr;
bagLWPrint();
}
}
I clear bagLW and refill it with the updated array.
void Inventory::bagLWPrint()
{
bagLW->clear();
for(int i = 0; i < bagSize; i++)
{
if(items[i] != nullptr)
{
bagLW->addItem(items[i]);
}
if(items[i] == nullptr)
{
bagLW->addItem(items[i]);
}
}
}
Other notes.
-The item only gets doubled when inserted into void Inventory::bagAddElement.
-I tried adding another item inside of void GameScreen::initGame and it did not double after deleting and re-entering.
Sorry for such a long question. And any help would be much, much appreciated.
void Inventory::bagExpand()
{
//1: INCREASE BAGSPACE
bagSize *= 2;
//2: CREATE TEMP ARRAY
QString *tempItems = new QString[bagSize];
//3: COPY OVER VALID VALUES FROM OLD ARRAY
for(int i = 0; i < nrOfEl; i++)
{
tempItems[i] = items[i];
}
//4: DELETE OLD ARRAY MEMORY
delete[] items;
//5: POINT OLD ARRAY POINTER TO NEW ARRAY LOCATION
items = tempItems;
//PRINT BAGLW - (UDPATE)
bagLWPrint();
qDebug()<<"Bag has increased";
}
The problem is that you connect and re-connect the getItemFromMapOneShop signal on every button click. That means that on first click, you add it once (signal triggered, 1 slot connected). On the second click, you add it twice (signal triggered, 2 slots connected). And so on.
QObjects manage a list of all connected slots per signal, and call each of them. Connecting a slot multiple times will call it that many times
You should connect the singal only once, e.g. in the constructor; or disconnect the signal when no longer used
As #Andéon Evain pointed out, you could also use Qt::UniqueConnection. This will not add a duplicate connection if it already exists (considering sender, signal, receiver, slot). That might be useful for cases where it's unknown if already connected; not in your simple case

Why do I lose my QList items when accessing it in another function?

GraphWidget Timeline Image.
I have a list called node1List which stores a list of nodes that are to be displayed in a QGraphicsScene. GraphWidget.cpp is a timeline where the user can click to add nodes. EffectsWidget is another timeline which has the option to add three QLabels to the scene. When these labels are added, I need to add a node to the graphWidget timeline in the same respective place.
list declaration in graphWidget.h:
private:
QList<Node *> node1List;
In graphWidget.cpp, on Mouse Click I create a node and add it to the list:
void GraphWidget::mousePressEvent(QMouseEvent *event)
{
switch (event->button()) {
case Qt::MouseButton::RightButton: {
//... create node
addNode1(newNode, location);
//...
}
break;
}
}
Here is the addNode1 function which is declared in the same graphWidget.cpp:
void GraphWidget::addNode1(Node *newNode,int loc)
{
node1List.insert(loc, newNode);
}
This all works fine and I can access the list of nodes WITHIN the mouse press event.
However now I need to allow these nodes to be also added when a label is added to another class, effectsWidget.cpp.
void EffectsWidget::addEffectsItemStart(){
graphWidget = new GraphWidget();
graphWidget->addLabelsToListAt(sceneStartLabel, 1);
//...
}
and then
void GraphWidget::addLabelsToListAt(QLabel *label, int location)
{
labelListGraphWidget.insert(location, label);
generateNodes(label);
}
So finally, now when I call generateNodes(label), I output the size of the node1List only to find that it is empty?
void GraphWidget::generateNodes(QLabel* label)
{
//The following outputs zero
DBOUT("List size = " << node1List.size() << "\n");
//...create new node
addNode1(newNode, location);
}
If anyone could please explain to me how to retain all the items in a list it would be great thanks! First question here so my apologies if I've messed it up!

QT - Can I dynamically point to GUI's labels using a variable passed into a method?

I'm creating an inventory and want to reference my inventory Forms QLabels with the name of the item passed into my method.
The QLabels and the names of the items are the same so I wanted to reduce if statements by referencing a QLabel depending on the name of the item passed in.
void Zork::on_btnTake_clicked()
{
Item *item = new Item(ui->takeItem->currentText().toStdString());
Colin->addItemToInventory(item);
inventory_screen->addItem(item->getDescription()); //Enables the Item in the Inventory
currentRoom->deleteItem(item);
ui->takeItem->removeItem(ui->takeItem->currentIndex()); //Only remove the item in the foreground of combobox at the time take button was clicked
}
Calls this Method addItemToInventory(item):
void Inventory_Ui:: addItem(string itemName){
myUi->itemName->setText(QString::fromStdString(itemName));
}
I am unable to do so as itemName is not a member of Ui, although itemName does contain members of Ui.
Example:
In my Inventory_Ui Form I have 6 labels, one named broken_bottle. I want to pass broken_bottle into the method's parameter itemName and them use that as a reference so at run-time it would look like myUi->broken_bottle->setText...
I think I follow what you are asking to do. I would access it with findChild or findChildren.
http://doc.qt.io/qt-5/qobject.html#findChild
http://doc.qt.io/qt-5/qobject.html#findChildren
QList <QLabel *> labels = ui->myForm()->findChildren<QLabel*>();
QLabel* targetLabel = 0;
foreach(QLabel * label, labels)
{
if(label->text() == ui->takeItem->currentText())
{
targetLabel = label;
break;
}
}
if(targetLabel != 0)
{
// ... do something with targetLabel
}
or...
QList <QLabel *> labels = ui->myForm()->findChildren<QLabel*>();
foreach(QLabel * label, labels)
label->setObjectName(label->text());
// later
QLabel * targetLabel = ui->myForm()->findChild<QLabel*>("text of label");
if(targetLabel)
// use it
Or if you are connecting all these labels to the same slot, you can figure some of it out using the sender static method.
http://doc.qt.io/qt-5/qobject.html#sender
void MainWindow::on_takeItem_currentTextChanged()
{
QLabel* targetLabel = qobject_cast<QLabel*>QObject::sender();
if(targetLabel)
// do something with it
}
Hope that helps.

qt QGraphicsScene additem

http://qt-project.org/doc/qt-4.8/qgraphicsscene.html#addItem
said
If the item is already in a different scene, it will first be removed from its old scene, and then added to this scene as a
top-level.
I want to keep the item in old scene.
how can I do this?
myscene1.addItem(item);
myscene2.addItem(item);// I don't want to remove item from myscene1
You can make a copy of the item :
myscene1.addItem(item);
myscene2.addItem(item->clone());
An item cannot occupy two scenes at the same time, in the way that you cannot be in two places at the same time.
The only way to do this is to make a copy of the item and place it in the second scene.
What you could do is to create a new class. e.g.
class Position
{
...
QPoinfF pos;
...
}
Then you can add that class to your item.
class Item : public QGraphicsItem
{
...
public:
void setSharedPos(Position *pos)
{
sharedPosition = pos;
}
//implement the paint(...) function
//its beeing called by the scene
void paint(...)
{
//set the shared position here
setPos(sharedPos);
//paint the item
...
}
protected:
void QGraphicsItem::mouseReleaseEvent ( QGraphicsSceneMouseEvent * event )
{
//get the position from the item that could have been moved
//you could also check if the position actually changed
sharedPosition->pos = pos();
}
private
Position *sharedPostion;
...
}
You would no have to create two items and give them both the same pointer to a Position object.
Item *item1 = new Item;
Item *item2 = new Item;
Position *sharedPos = new Position;
item1->setSharedPos(sharedPos);
item2->setSharedPos(sharedPos);
myScene1->addItem(item1);
myScene2->addItem(item2);
They should no at least share their position in the scenes.
If this works then you'll have to change the Position class to fit your needs and it should be working.
I am not quite shure if setting the position in the paint() function works. But thats how I would try to synchronise the items. If it does not work then you'll have to look for another place to update the settings of the items.
Or you could give the items a Pointer to each other and let them change the positions/settings directly.
e.g.
class Item : public QGraphicsItem
{
...
void QGraphicsItem::mouseReleaseEvent ( QGraphicsSceneMouseEvent * event )
{
otherItem->setPos(pos());
}
...
void setOtherItem(Item *item)
{
otherItem = item;
}
private:
Item *otherItem;
}
Item *item1 = new Item;
Item *item2 = new Item;
item1->setOtherItem(item2);
item2->setOtherItem(item1);
myScene1->addItem(item1);
myScene2->addItem(item2);